diff --git a/.github/workflows/build.yml b/.gitea/workflows/build.yml similarity index 51% rename from .github/workflows/build.yml rename to .gitea/workflows/build.yml index 1118afa..71d3c1b 100644 --- a/.github/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -1,8 +1,6 @@ name: Build Nginx-based docker image on: push: - branches: - - main concurrency: group: ${{ github.ref }} cancel-in-progress: true @@ -24,13 +22,22 @@ jobs: registry: ${{ secrets.PRIVATE_REGISTRY_HOST }} username: ${{ secrets.PRIVATE_REGISTRY_USERNAME }} password: ${{ secrets.PRIVATE_REGISTRY_TOKEN }} - - - name: Build and push + + - name: Build and push intermediate stages (build) uses: docker/build-push-action@v6 with: context: . - push: ${{ github.event_name != 'pull_request' }} - tags: | - ${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:latest - cache-from: type=registry,ref=user/app:latest + target: build + push: ${{ github.ref == 'refs/heads/main' }} + tags: ${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:build + cache-from: type=registry,ref=${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:build + cache-to: type=inline + + - name: Build and push (final) + uses: docker/build-push-action@v6 + with: + context: . + push: ${{ github.ref == 'refs/heads/main' }} + tags: ${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:latest + cache-from: type=registry,ref=${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:latest cache-to: type=inline \ No newline at end of file diff --git a/.github/workflows/copyright_notice.yml b/.gitea/workflows/copyright_notice.yml similarity index 100% rename from .github/workflows/copyright_notice.yml rename to .gitea/workflows/copyright_notice.yml diff --git a/.github/workflows/license_finder.yml b/.gitea/workflows/license_finder.yml similarity index 100% rename from .github/workflows/license_finder.yml rename to .gitea/workflows/license_finder.yml diff --git a/.github/workflows/tests.yml b/.gitea/workflows/tests.yml similarity index 95% rename from .github/workflows/tests.yml rename to .gitea/workflows/tests.yml index 36f5b06..6166ed7 100644 --- a/.github/workflows/tests.yml +++ b/.gitea/workflows/tests.yml @@ -24,6 +24,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} - uses: ruby/setup-ruby@v1.202.0 - run: bundle install + - run: bundle exec rubocop --force-exclusion --parallel - name: Wait until Postgres is ready to accept connections run: | apt-get update && apt-get install -f -y postgresql-client diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..ca1ed81 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,29 @@ +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 \ No newline at end of file diff --git a/Gemfile b/Gemfile index cbb63fd..adaab29 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source 'https://rubygems.org' ruby '3.4.1' @@ -15,15 +17,15 @@ gem 'stimulus-rails' gem 'turbo-rails' gem 'tzinfo-data', platforms: %i[windows jruby] +gem 'acts_as_tenant' gem 'faker' +gem 'httparty' gem 'jsonapi-rails' +gem 'pluck_to_hash' gem 'rack-cors' gem 'react-rails' -gem 'rubytree' -gem 'acts_as_tenant' -gem 'httparty' gem 'rswag' -gem 'pluck_to_hash' +gem 'rubytree' group :development, :test do gem 'annotaterb' @@ -36,12 +38,16 @@ group :development, :test do end group :development do - gem 'rubocop' - gem 'web-console' gem 'letter_opener_web' + 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' end gem 'chroma' gem 'solid_queue', '~> 1.0' -gem "devise", "~> 4.9" +gem 'devise', '~> 4.9' diff --git a/Gemfile.lock b/Gemfile.lock index f55a087..4295b50 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -85,7 +85,7 @@ GEM base64 (0.2.0) bcrypt (3.1.20) benchmark (0.4.0) - bigdecimal (3.1.8) + bigdecimal (3.1.9) bindex (0.8.1) bootsnap (1.18.4) msgpack (~> 1.2) @@ -339,6 +339,18 @@ GEM unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.36.2) 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) rubytree (2.1.0) json (~> 2.0, > 2.3.1) @@ -346,7 +358,7 @@ GEM securerandom (0.4.1) shoulda-matchers (6.4.0) activesupport (>= 5.2.0) - solid_queue (1.1.0) + solid_queue (1.1.2) activejob (>= 7.1) activerecord (>= 7.1) concurrent-ruby (>= 1.3.1) @@ -426,6 +438,10 @@ DEPENDENCIES rspec-rails (~> 7.1.0) rswag rubocop + rubocop-factory_bot + rubocop-rails + rubocop-rspec + rubocop-rspec_rails rubytree shoulda-matchers (~> 6.0) solid_queue (~> 1.0) @@ -456,7 +472,7 @@ CHECKSUMS base64 (0.2.0) sha256=0f25e9b21a02a0cc0cea8ef92b2041035d39350946e8789c562b2d1a3da01507 bcrypt (3.1.20) sha256=8410f8c7b3ed54a3c00cd2456bf13917d695117f033218e2483b2e40b0784099 benchmark (0.4.0) sha256=0f12f8c495545e3710c3e4f0480f63f06b4c842cc94cec7f33a956f5180e874a - bigdecimal (3.1.8) sha256=a89467ed5a44f8ae01824af49cbc575871fa078332e8f77ea425725c1ffe27be + bigdecimal (3.1.9) sha256=2ffc742031521ad69c2dfc815a98e426a230a3d22aeac1995826a75dabfad8cc bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e bootsnap (1.18.4) sha256=ac4c42af397f7ee15521820198daeff545e4c360d2772c601fbdc2c07d92af55 builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f @@ -561,12 +577,16 @@ CHECKSUMS rswag-ui (2.16.0) sha256=a1f49e927dceda92e6e6e7c1000f1e217ee66c565f69e28131dc98b33cd3a04f rubocop (1.69.2) sha256=762fb0f30a379bf6054588d39f1815a2a1df8f067bc0337d3fe500e346924bb8 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 rubytree (2.1.0) sha256=30e8759ba060dff0dabf7e40cbaaa4df892fa34cbe9f1b3fbb00e83a3f321e4b rubyzip (2.3.2) sha256=3f57e3935dc2255c414484fbf8d673b4909d8a6a57007ed754dde39342d2373f securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 shoulda-matchers (6.4.0) sha256=9055bb7f4bb342125fb860809798855c630e05ef5e75837b3168b8e6ee1608b0 - solid_queue (1.1.0) sha256=55c7b1d65f2943ec8f3deee36fcb0cae91d5fa60410b294775db9da22be8353c + solid_queue (1.1.2) sha256=178c9396d1cf0dac595c7508da90ddb397d25848ca007b5c5ed48e6ac6fc360c sprockets (4.2.1) sha256=951b13dd2f2fcae840a7184722689a803e0ff9d2702d902bd844b196da773f97 sprockets-rails (3.5.2) sha256=a9e88e6ce9f8c912d349aa5401509165ec42326baf9e942a85de4b76dbc4119e stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06 diff --git a/Rakefile b/Rakefile index 9a5ea73..488c551 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,8 @@ +# frozen_string_literal: true + # 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. -require_relative "config/application" +require_relative 'config/application' Rails.application.load_tasks diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb index 3073f1e..72b2bf5 100644 --- a/app/channels/application_cable/channel.rb +++ b/app/channels/application_cable/channel.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + module ApplicationCable class Channel < ActionCable::Channel::Base end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 1cc97c6..d45ff4d 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + module ApplicationCable class Connection < ActionCable::Connection::Base end diff --git a/app/controllers/affinities_controller.rb b/app/controllers/affinities_controller.rb new file mode 100644 index 0000000..f8b7052 --- /dev/null +++ b/app/controllers/affinities_controller.rb @@ -0,0 +1,41 @@ +# 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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7cb39a8..b1baa15 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + class ApplicationController < ActionController::Base set_current_tenant_through_filter before_action :set_tenant @@ -40,9 +42,9 @@ class ApplicationController < ActionController::Base end def captcha_params - params.expect(captcha: [:id, :answer]) + params.expect(captcha: %i[id answer]) end - + def default_url_options(options = {}) options.merge(path_params: { slug: ActsAsTenant.current_tenant&.slug }) end @@ -53,7 +55,7 @@ class ApplicationController < ActionController::Base def development_swagger? 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 def set_csrf_cookie diff --git a/app/controllers/captcha_controller.rb b/app/controllers/captcha_controller.rb index 351b324..57dac61 100644 --- a/app/controllers/captcha_controller.rb +++ b/app/controllers/captcha_controller.rb @@ -1,10 +1,12 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + class CaptchaController < ApplicationController skip_before_action :authenticate_user! skip_before_action :set_tenant def create - id = LibreCaptcha.new.get_id + id = LibreCaptcha.new.id render json: { id:, media_url: media_captcha_index_url(id:) diff --git a/app/controllers/expenses_controller.rb b/app/controllers/expenses_controller.rb index 2a125d9..6e1b63b 100644 --- a/app/controllers/expenses_controller.rb +++ b/app/controllers/expenses_controller.rb @@ -1,12 +1,14 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + class ExpensesController < ApplicationController def summary render json: Expenses::TotalQuery.new.call end def index - render json: Expense.all.order(pricing_type: :asc, amount: :desc).as_json(only: %i[id name amount pricing_type]) + render json: Expense.order(pricing_type: :asc, amount: :desc).as_json(only: %i[id name amount pricing_type]) end def create diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index f53ed35..77a347f 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + class GroupsController < ApplicationController def index query_result = Groups::SummaryQuery.new.call.as_json.map(&:deep_symbolize_keys).map do |group| @@ -39,6 +41,6 @@ class GroupsController < ApplicationController end def group_params - params.expect(group: [:name, :icon, :color]) + params.expect(group: %i[name icon color]) end end diff --git a/app/controllers/guests_controller.rb b/app/controllers/guests_controller.rb index 54975b6..e7f9ff0 100644 --- a/app/controllers/guests_controller.rb +++ b/app/controllers/guests_controller.rb @@ -1,10 +1,12 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require 'csv' class GuestsController < ApplicationController def index - render json: Guest.all.includes(:group) + render json: Guest.includes(:group) .left_joins(:group) .order('groups.name' => :asc, name: :asc) .as_json(only: %i[id name status], include: { group: { only: %i[id name] } }) diff --git a/app/controllers/summary_controller.rb b/app/controllers/summary_controller.rb index 55983dc..e165127 100644 --- a/app/controllers/summary_controller.rb +++ b/app/controllers/summary_controller.rb @@ -1,29 +1,43 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + 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 - } + 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 + + { + projected: { + total: expense_summary['total_projected'], + guests: expense_summary['projected_guests'] }, - 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 + confirmed: { + total: expense_summary['total_confirmed'], + guests: expense_summary['confirmed_guests'] + }, + status: { + paid: 0 } } end diff --git a/app/controllers/tables_arrangements_controller.rb b/app/controllers/tables_arrangements_controller.rb index 00ba8b1..23b99ba 100644 --- a/app/controllers/tables_arrangements_controller.rb +++ b/app/controllers/tables_arrangements_controller.rb @@ -1,8 +1,10 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + class TablesArrangementsController < ApplicationController def index - render json: TablesArrangement.all.order(discomfort: :asc).limit(3).as_json(only: %i[id name discomfort]) + render json: TablesArrangement.order(discomfort: :asc).limit(3).as_json(only: %i[id name discomfort]) end def show diff --git a/app/controllers/tokens_controller.rb b/app/controllers/tokens_controller.rb index c3dabba..5a8490d 100644 --- a/app/controllers/tokens_controller.rb +++ b/app/controllers/tokens_controller.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + class TokensController < ApplicationController skip_before_action :authenticate_user! skip_before_action :set_tenant diff --git a/app/controllers/users/confirmations_controller.rb b/app/controllers/users/confirmations_controller.rb index 660d263..7e5c048 100644 --- a/app/controllers/users/confirmations_controller.rb +++ b/app/controllers/users/confirmations_controller.rb @@ -1,23 +1,27 @@ # Copyright (C) 2024 Manuel Bustillo -class Users::ConfirmationsController < Devise::ConfirmationsController - clear_respond_to - respond_to :json +# frozen_string_literal: true - def show - super do |resource| - if resource.errors.empty? - respond_to do |format| - format.json { render json: resource, status: :ok } - format.any { redirect_to root_path } +module Users + class ConfirmationsController < Devise::ConfirmationsController + clear_respond_to + respond_to :json + + def show + super do |resource| + if resource.errors.empty? + respond_to do |format| + format.json { render json: resource, status: :ok } + format.any { redirect_to root_path } + end + else + render json: { + message: 'Record invalid', + errors: resource.errors.full_messages + }, status: :unprocessable_entity end - else - render json: { - message: 'Record invalid', - errors: resource.errors.full_messages - }, status: :unprocessable_entity + return end - return end end -end \ No newline at end of file +end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 0aae89d..0849d96 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -1,28 +1,32 @@ # Copyright (C) 2024 Manuel Bustillo -class Users::RegistrationsController < Devise::RegistrationsController - clear_respond_to - respond_to :json +# frozen_string_literal: true - before_action :validate_captcha!, only: :create +module Users + class RegistrationsController < Devise::RegistrationsController + clear_respond_to + respond_to :json - def create - wedding = Wedding.create(slug: params[:slug]) - unless wedding.persisted? - render json: { errors: wedding.errors.full_messages }, status: :unprocessable_entity - return - end + before_action :validate_captcha!, only: :create - ActsAsTenant.with_tenant(wedding) do - super do |user| - wedding.destroy unless user.persisted? + def create + wedding = Wedding.create(slug: params[:slug]) + unless wedding.persisted? + render json: { errors: wedding.errors.full_messages }, status: :unprocessable_entity + return + end + + ActsAsTenant.with_tenant(wedding) do + super do |user| + wedding.destroy unless user.persisted? + end end end - end - private + private - def set_tenant - set_current_tenant(nil) + def set_tenant + set_current_tenant(nil) + end end -end \ No newline at end of file +end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 0de9af5..5e3ad06 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -1,6 +1,10 @@ # Copyright (C) 2024 Manuel Bustillo -class Users::SessionsController < Devise::SessionsController - clear_respond_to - respond_to :json -end \ No newline at end of file +# frozen_string_literal: true + +module Users + class SessionsController < Devise::SessionsController + clear_respond_to + respond_to :json + end +end diff --git a/app/extensions/tree_node_extension.rb b/app/extensions/tree_node_extension.rb index 630b011..9bd1ed4 100644 --- a/app/extensions/tree_node_extension.rb +++ b/app/extensions/tree_node_extension.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + module TreeNodeExtension def distance_to_common_ancestor(another_node) return 0 if self == another_node diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 5e869ad..1bc40d8 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,4 +1,6 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + module ApplicationHelper end diff --git a/app/helpers/expenses_helper.rb b/app/helpers/expenses_helper.rb index af8a78e..287c70f 100644 --- a/app/helpers/expenses_helper.rb +++ b/app/helpers/expenses_helper.rb @@ -1,4 +1,6 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + module ExpensesHelper end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 311c1a2..b0492f2 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -1,4 +1,6 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + module GroupsHelper end diff --git a/app/helpers/guests_helper.rb b/app/helpers/guests_helper.rb index ca740f6..849ddb8 100644 --- a/app/helpers/guests_helper.rb +++ b/app/helpers/guests_helper.rb @@ -1,4 +1,6 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + module GuestsHelper end diff --git a/app/helpers/tables_arrangements_helper.rb b/app/helpers/tables_arrangements_helper.rb index 637e079..bd95809 100644 --- a/app/helpers/tables_arrangements_helper.rb +++ b/app/helpers/tables_arrangements_helper.rb @@ -1,4 +1,6 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + module TablesArrangementsHelper end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index 3498a49..02ef80e 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + class ApplicationJob < ActiveJob::Base # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked diff --git a/app/jobs/table_simulator_job.rb b/app/jobs/table_simulator_job.rb index 7cbe659..50fe728 100644 --- a/app/jobs/table_simulator_job.rb +++ b/app/jobs/table_simulator_job.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + class TableSimulatorJob < ApplicationJob queue_as :default diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 94e1656..b168ff3 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,6 +1,8 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + class ApplicationMailer < ActionMailer::Base - default from: "from@example.com" - layout "mailer" + default from: 'from@example.com' + layout 'mailer' end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index e93781d..1a28db5 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + class ApplicationRecord < ActiveRecord::Base primary_abstract_class end diff --git a/app/models/expense.rb b/app/models/expense.rb index 5601c30..a34cfc5 100644 --- a/app/models/expense.rb +++ b/app/models/expense.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + # == Schema Information # # Table name: expenses diff --git a/app/models/group.rb b/app/models/group.rb index a00a671..ddf6e52 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + # == Schema Information # # Table name: groups @@ -31,7 +33,7 @@ class Group < ApplicationRecord validates :name, uniqueness: true validates :name, :order, presence: true - has_many :children, class_name: 'Group', foreign_key: 'parent_id' + has_many :children, class_name: 'Group', foreign_key: 'parent_id', dependent: :nullify, inverse_of: :parent belongs_to :parent, class_name: 'Group', optional: true before_create :set_color @@ -41,10 +43,7 @@ class Group < ApplicationRecord 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 - - children.zip(derived_colors) do |child, raw_color| - + children.zip(palette(generation)) do |child, raw_color| final_color = raw_color.paint final_color.brighten(60) if final_color.dark? @@ -54,8 +53,20 @@ class Group < ApplicationRecord end end + def affinities + GroupAffinity.where(group_a_id: id).or(GroupAffinity.where(group_b_id: id)) + end + 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 return if color.present? diff --git a/app/models/group_affinity.rb b/app/models/group_affinity.rb new file mode 100644 index 0000000..1e85485 --- /dev/null +++ b/app/models/group_affinity.rb @@ -0,0 +1,43 @@ +# 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 diff --git a/app/models/guest.rb b/app/models/guest.rb index a0a2992..8687767 100644 --- a/app/models/guest.rb +++ b/app/models/guest.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + # == Schema Information # # Table name: guests @@ -39,8 +41,8 @@ class Guest < ApplicationRecord scope :potential, -> { where.not(status: %i[declined considered]) } - after_save :recalculate_simulations, if: :saved_change_to_status? after_destroy :recalculate_simulations + after_save :recalculate_simulations, if: :saved_change_to_status? has_many :seats, dependent: :delete_all diff --git a/app/models/seat.rb b/app/models/seat.rb index 3f13629..a00ec99 100644 --- a/app/models/seat.rb +++ b/app/models/seat.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + # == Schema Information # # Table name: seats diff --git a/app/models/tables_arrangement.rb b/app/models/tables_arrangement.rb index 3080e49..72f13d5 100644 --- a/app/models/tables_arrangement.rb +++ b/app/models/tables_arrangement.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + # == Schema Information # # Table name: tables_arrangements @@ -21,7 +23,7 @@ # class TablesArrangement < ApplicationRecord acts_as_tenant :wedding - has_many :seats + has_many :seats, dependent: :delete_all has_many :guests, through: :seats before_create :assign_name diff --git a/app/models/user.rb b/app/models/user.rb index 43c3a9e..5a9683e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + # == Schema Information # # Table name: users diff --git a/app/models/wedding.rb b/app/models/wedding.rb index 125c58e..c94d83d 100644 --- a/app/models/wedding.rb +++ b/app/models/wedding.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + # == Schema Information # # Table name: weddings diff --git a/app/queries/expenses/total_query.rb b/app/queries/expenses/total_query.rb index 7dc47d6..fc46a6b 100644 --- a/app/queries/expenses/total_query.rb +++ b/app/queries/expenses/total_query.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + module Expenses class TotalQuery private attr_reader :wedding @@ -16,7 +18,7 @@ module Expenses private def query - <<~SQL + <<~SQL.squish WITH guest_count AS (#{guest_count_per_status}), expense_summary AS (#{expense_summary}) SELECT guest_count.confirmed as confirmed_guests, @@ -28,7 +30,7 @@ module Expenses end def expense_summary - <<~SQL + <<~SQL.squish SELECT coalesce(sum(amount) filter (where pricing_type = 'fixed'), 0) as fixed, coalesce(sum(amount) filter (where pricing_type = 'per_person'), 0) as variable FROM expenses @@ -37,7 +39,7 @@ module Expenses end def guest_count_per_status - <<~SQL + <<~SQL.squish 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 diff --git a/app/queries/groups/summary_query.rb b/app/queries/groups/summary_query.rb index 0736496..28fe6a5 100644 --- a/app/queries/groups/summary_query.rb +++ b/app/queries/groups/summary_query.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + module Groups class SummaryQuery def call @@ -9,13 +11,21 @@ module Groups :icon, :parent_id, :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 = 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'), - ) + Arel.sql('count(*) filter (where status = 40) as tentative') + ] end end end diff --git a/app/serializers/serializable_group.rb b/app/serializers/serializable_group.rb index f7cc103..eb30b10 100644 --- a/app/serializers/serializable_group.rb +++ b/app/serializers/serializable_group.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + class SerializableGroup < JSONAPI::Serializable::Resource type 'group' diff --git a/app/serializers/serializable_guest.rb b/app/serializers/serializable_guest.rb index e411307..981e58b 100644 --- a/app/serializers/serializable_guest.rb +++ b/app/serializers/serializable_guest.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + class SerializableGuest < JSONAPI::Serializable::Resource type 'guest' diff --git a/app/services/affinity_groups_hierarchy.rb b/app/services/affinity_groups_hierarchy.rb index e20de97..310bac8 100644 --- a/app/services/affinity_groups_hierarchy.rb +++ b/app/services/affinity_groups_hierarchy.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + class AffinityGroupsHierarchy < Array include Singleton diff --git a/app/services/libre_captcha.rb b/app/services/libre_captcha.rb index 8684e9f..314c186 100644 --- a/app/services/libre_captcha.rb +++ b/app/services/libre_captcha.rb @@ -1,20 +1,20 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + class LibreCaptcha - def get_id - HTTParty.post("http://libre-captcha:8888/v2/captcha", - body: { - input_type: "text", - level: :hard, - media: 'image/png', - size: '350x100' - }.to_json - ).then { |raw| JSON.parse(raw)['id'] } + def id + HTTParty.post('http://libre-captcha:8888/v2/captcha', + body: { + input_type: 'text', + level: :hard, + media: 'image/png', + size: '350x100' + }.to_json).then { |raw| JSON.parse(raw)['id'] } end def valid?(id:, answer:) - HTTParty.post("http://libre-captcha:8888/v2/answer", - body: { id:, answer: }.to_json - ).then { |raw| JSON.parse(raw)['result'] == 'True' } + HTTParty.post('http://libre-captcha:8888/v2/answer', + body: { id:, answer: }.to_json).then { |raw| JSON.parse(raw)['result'] == 'True' } end -end \ No newline at end of file +end diff --git a/app/services/tables/discomfort_calculator.rb b/app/services/tables/discomfort_calculator.rb index 35da8ce..48dacce 100644 --- a/app/services/tables/discomfort_calculator.rb +++ b/app/services/tables/discomfort_calculator.rb @@ -1,7 +1,19 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + module Tables 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 def initialize(table:) @table = table @@ -45,12 +57,7 @@ module Tables # def cohesion_discomfort table.map(&:group_id).tally.to_a.combination(2).sum do |(a, count_a), (b, count_b)| - distance = AffinityGroupsHierarchy.instance.distance(a, b) - - next count_a * count_b if distance.nil? - next 0 if distance.zero? - - count_a * count_b * Rational(distance, distance + 1) + count_a * count_b * self.class.cohesion_discomfort(id_a: a, id_b: b) end end end diff --git a/app/services/tables/distribution.rb b/app/services/tables/distribution.rb index 9236007..04d1e6a 100644 --- a/app/services/tables/distribution.rb +++ b/app/services/tables/distribution.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require_relative '../../extensions/tree_node_extension' module Tables diff --git a/app/services/tables/shift.rb b/app/services/tables/shift.rb index 6ae7c94..2326b1e 100644 --- a/app/services/tables/shift.rb +++ b/app/services/tables/shift.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + module Tables class Shift private attr_reader :initial_solution diff --git a/app/services/tables/swap.rb b/app/services/tables/swap.rb index ae74294..1fe6944 100644 --- a/app/services/tables/swap.rb +++ b/app/services/tables/swap.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + module Tables class Swap private attr_reader :initial_solution diff --git a/app/services/tables/table.rb b/app/services/tables/table.rb index 2c50435..8877e7c 100644 --- a/app/services/tables/table.rb +++ b/app/services/tables/table.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + module Tables class Table < Set attr_accessor :discomfort, :min_per_table, :max_per_table diff --git a/app/services/vns/engine.rb b/app/services/vns/engine.rb index ee50d45..9f600df 100644 --- a/app/services/vns/engine.rb +++ b/app/services/vns/engine.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + module VNS class Engine class << self diff --git a/config.ru b/config.ru index 4a3c09a..6dc8321 100644 --- a/config.ru +++ b/config.ru @@ -1,6 +1,8 @@ +# frozen_string_literal: true + # This file is used by Rack-based servers to start the application. -require_relative "config/environment" +require_relative 'config/environment' run Rails.application Rails.application.load_server diff --git a/config/routes.rb b/config/routes.rb index c3bc335..9e2b004 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -23,7 +23,12 @@ Rails.application.routes.draw do get '/users/confirmation', to: 'users/confirmations#show', as: :confirmation end - resources :groups, only: %i[index create update destroy] + resources :groups, only: %i[index create update destroy] do + resources :affinities, only: %i[index] do + put :bulk_update, on: :collection + end + end + resources :guests, only: %i[index create update destroy] do post :bulk_update, on: :collection end diff --git a/db/migrate/20241216231415_create_group_affinities.rb b/db/migrate/20241216231415_create_group_affinities.rb new file mode 100644 index 0000000..2b03897 --- /dev/null +++ b/db/migrate/20241216231415_create_group_affinities.rb @@ -0,0 +1,29 @@ +# 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 diff --git a/db/schema.rb b/db/schema.rb index 0684e19..f723b74 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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_08_102932) do +ActiveRecord::Schema[8.0].define(version: 2024_12_16_231415) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -30,6 +30,19 @@ ActiveRecord::Schema[8.0].define(version: 2024_12_08_102932) do t.index ["wedding_id"], name: "index_expenses_on_wedding_id" 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| t.string "name", null: false t.string "icon" @@ -228,6 +241,8 @@ ActiveRecord::Schema[8.0].define(version: 2024_12_08_102932) do end 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", "weddings" add_foreign_key "guests", "groups" diff --git a/docker-compose.yml b/docker-compose.yml index df2c035..93cff59 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,13 @@ services: environment: DATABASE_URL: postgres://postgres:postgres@db:5432/postgres RAILS_ENV: development + tty: true + stdin_open: true + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/up"] + interval: 10s + timeout: 5s + retries: 5 volumes: - .:/rails workers: @@ -32,6 +39,11 @@ services: dockerfile: Dockerfile.dev ports: - 3000 + healthcheck: + test: wget -qO - http://localhost:3000/api/health || exit 1 + interval: 10s + timeout: 5s + retries: 5 depends_on: - backend volumes: @@ -50,8 +62,10 @@ services: volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro depends_on: - - frontend - - backend + frontend: + condition: service_healthy + backend: + condition: service_healthy db: image: postgres:17 ports: diff --git a/lib/tasks/annotate_rb.rake b/lib/tasks/annotate_rb.rake index 1ad0ec3..e8368b2 100644 --- a/lib/tasks/annotate_rb.rake +++ b/lib/tasks/annotate_rb.rake @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # This rake task was added by annotate_rb gem. # Can set `ANNOTATERB_SKIP_ON_DB_TASKS` to be anything to skip this -if Rails.env.development? && ENV["ANNOTATERB_SKIP_ON_DB_TASKS"].nil? - require "annotate_rb" +if Rails.env.development? && ENV['ANNOTATERB_SKIP_ON_DB_TASKS'].nil? + require 'annotate_rb' AnnotateRb::Core.load_rake_tasks end diff --git a/spec/extensions/tree_spec.rb b/spec/extensions/tree_spec.rb index 6629197..1d3e6d0 100644 --- a/spec/extensions/tree_spec.rb +++ b/spec/extensions/tree_spec.rb @@ -1,70 +1,72 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require 'rails_helper' module Tree RSpec.describe TreeNode do describe '#distance_to_common_ancestor' do - def assert_distance(node_1, node_2, distance) + def assert_distance(node1, node2, distance) aggregate_failures do - expect(node_1.distance_to_common_ancestor(node_2)).to eq(distance) - expect(node_2.distance_to_common_ancestor(node_1)).to eq(distance) + expect(node1.distance_to_common_ancestor(node2)).to eq(distance) + expect(node2.distance_to_common_ancestor(node1)).to eq(distance) end end context 'when the two nodes are the same' do it 'returns 0 when comparing the root itself' do - root = Tree::TreeNode.new('root') + root = described_class.new('root') assert_distance(root, root, 0) end it 'returns 0 when comparing a child to itself' do - root = Tree::TreeNode.new('root') - child = root << Tree::TreeNode.new('child') + root = described_class.new('root') + child = root << described_class.new('child') assert_distance(child, child, 0) end end context 'when the two nodes are siblings' do it 'returns 1 when comparing siblings' do - root = Tree::TreeNode.new('root') - child1 = root << Tree::TreeNode.new('child1') - child2 = root << Tree::TreeNode.new('child2') + root = described_class.new('root') + child1 = root << described_class.new('child1') + child2 = root << described_class.new('child2') assert_distance(child1, child2, 1) end end context 'when one node is parent of the other' do it 'returns 1 when comparing parent to child' do - root = Tree::TreeNode.new('root') - child = root << Tree::TreeNode.new('child') + root = described_class.new('root') + child = root << described_class.new('child') assert_distance(root, child, 1) end end context 'when one node is grandparent of the other' do it 'returns 2 when comparing grandparent to grandchild' do - root = Tree::TreeNode.new('root') - child = root << Tree::TreeNode.new('child') - grandchild = child << Tree::TreeNode.new('grandchild') + root = described_class.new('root') + child = root << described_class.new('child') + grandchild = child << described_class.new('grandchild') assert_distance(root, grandchild, 2) end end context 'when the two nodes are cousins' do it 'returns 2 when comparing cousins' do - root = Tree::TreeNode.new('root') - child1 = root << Tree::TreeNode.new('child1') - child2 = root << Tree::TreeNode.new('child2') - grandchild1 = child1 << Tree::TreeNode.new('grandchild1') - grandchild2 = child2 << Tree::TreeNode.new('grandchild2') + root = described_class.new('root') + child1 = root << described_class.new('child1') + child2 = root << described_class.new('child2') + grandchild1 = child1 << described_class.new('grandchild1') + grandchild2 = child2 << described_class.new('grandchild2') assert_distance(grandchild1, grandchild2, 2) end end context 'when the two nodes are not related' do it 'returns nil' do - root = Tree::TreeNode.new('root') - another_root = Tree::TreeNode.new('another_root') + root = described_class.new('root') + another_root = described_class.new('another_root') assert_distance(root, another_root, nil) end end diff --git a/spec/factories/expense.rb b/spec/factories/expense.rb index 726d94d..e122a43 100644 --- a/spec/factories/expense.rb +++ b/spec/factories/expense.rb @@ -1,19 +1,22 @@ # Copyright (C) 2024 Manuel Bustillo +# Copyright (C) 2024 Manuel Bustillo + +# frozen_string_literal: true + FactoryBot.define do - factory :expense do - wedding - sequence(:name) { |i| "Expense #{i}" } - pricing_type { "fixed" } - amount { 100 } - end - - trait :fixed do - pricing_type { "fixed" } - end - - trait :per_person do - pricing_type { "per_person" } - end + factory :expense do + wedding + sequence(:name) { |i| "Expense #{i}" } + pricing_type { 'fixed' } + amount { 100 } end - \ No newline at end of file + + trait :fixed do + pricing_type { 'fixed' } + end + + trait :per_person do + pricing_type { 'per_person' } + end +end diff --git a/spec/factories/group_affinities.rb b/spec/factories/group_affinities.rb new file mode 100644 index 0000000..f5f2b65 --- /dev/null +++ b/spec/factories/group_affinities.rb @@ -0,0 +1,13 @@ +# 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 diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index ccd1156..84ae9ec 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -1,5 +1,9 @@ # Copyright (C) 2024 Manuel Bustillo +# Copyright (C) 2024 Manuel Bustillo + +# frozen_string_literal: true + FactoryBot.define do factory :group do wedding diff --git a/spec/factories/guest.rb b/spec/factories/guest.rb index 28f3b80..25a5c5e 100644 --- a/spec/factories/guest.rb +++ b/spec/factories/guest.rb @@ -1,5 +1,9 @@ # Copyright (C) 2024 Manuel Bustillo +# Copyright (C) 2024 Manuel Bustillo + +# frozen_string_literal: true + FactoryBot.define do factory :guest do group diff --git a/spec/factories/table_arrangement.rb b/spec/factories/table_arrangement.rb index 693e68a..19c5f14 100644 --- a/spec/factories/table_arrangement.rb +++ b/spec/factories/table_arrangement.rb @@ -1,8 +1,11 @@ # Copyright (C) 2024 Manuel Bustillo +# Copyright (C) 2024 Manuel Bustillo + +# frozen_string_literal: true + FactoryBot.define do factory :tables_arrangement do wedding end end - \ No newline at end of file diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 2aa8a37..3616935 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -1,5 +1,9 @@ # Copyright (C) 2024 Manuel Bustillo +# Copyright (C) 2024 Manuel Bustillo + +# frozen_string_literal: true + FactoryBot.define do factory :user do wedding diff --git a/spec/factories/weddings.rb b/spec/factories/weddings.rb index 5a765a1..1e6f555 100644 --- a/spec/factories/weddings.rb +++ b/spec/factories/weddings.rb @@ -1,5 +1,9 @@ # Copyright (C) 2024 Manuel Bustillo +# Copyright (C) 2024 Manuel Bustillo + +# frozen_string_literal: true + FactoryBot.define do factory :wedding do sequence(:slug) { |i| "wedding-#{i}" } diff --git a/spec/models/expense_spec.rb b/spec/models/expense_spec.rb index 973ccf2..b002e78 100644 --- a/spec/models/expense_spec.rb +++ b/spec/models/expense_spec.rb @@ -1,12 +1,16 @@ # Copyright (C) 2024 Manuel Bustillo +# Copyright (C) 2024 Manuel Bustillo + +# frozen_string_literal: true + require 'rails_helper' -RSpec.describe Expense, type: :model do +RSpec.describe Expense do describe 'validations' do - it { should validate_presence_of(:name) } - it { should validate_presence_of(:amount) } - it { should validate_numericality_of(:amount).is_greater_than(0) } - it { should validate_presence_of(:pricing_type) } + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_presence_of(:amount) } + it { is_expected.to validate_numericality_of(:amount).is_greater_than(0) } + it { is_expected.to validate_presence_of(:pricing_type) } end end diff --git a/spec/models/group_affinity_spec.rb b/spec/models/group_affinity_spec.rb new file mode 100644 index 0000000..9d56d08 --- /dev/null +++ b/spec/models/group_affinity_spec.rb @@ -0,0 +1,46 @@ +# 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 diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 10102f4..f7e7fd3 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -1,10 +1,14 @@ # Copyright (C) 2024 Manuel Bustillo +# Copyright (C) 2024 Manuel Bustillo + +# frozen_string_literal: true + require 'rails_helper' -RSpec.describe Group, type: :model do +RSpec.describe Group do describe 'callbacks' do - it 'should set color before create' do + it 'sets color before create' do expect(create(:group).color).to be_present end end diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb index 6786271..a63da70 100644 --- a/spec/models/guest_spec.rb +++ b/spec/models/guest_spec.rb @@ -1,12 +1,17 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require 'rails_helper' -RSpec.describe Guest, type: :model do +RSpec.describe Guest do describe 'validations' do - it { should validate_presence_of(:name) } + subject(:guest) { build(:guest) } + + it { is_expected.to validate_presence_of(:name) } + it do - should define_enum_for(:status).with_values( + expect(guest).to define_enum_for(:status).with_values( considered: 0, invited: 10, confirmed: 20, @@ -16,7 +21,7 @@ RSpec.describe Guest, type: :model do end end - it { should belong_to(:group).optional } + it { is_expected.to belong_to(:group).optional } describe 'scopes' do describe '.potential' do @@ -27,7 +32,7 @@ RSpec.describe Guest, type: :model do confirmed_guest = create(:guest, status: :confirmed) tentative_guest = create(:guest, status: :tentative) - expect(Guest.potential).to match_array([invited_guest, confirmed_guest, tentative_guest]) + expect(described_class.potential).to contain_exactly(invited_guest, confirmed_guest, tentative_guest) end end end diff --git a/spec/models/seat_spec.rb b/spec/models/seat_spec.rb index 2020f33..40e63f2 100644 --- a/spec/models/seat_spec.rb +++ b/spec/models/seat_spec.rb @@ -1,7 +1,11 @@ # Copyright (C) 2024 Manuel Bustillo +# Copyright (C) 2024 Manuel Bustillo + +# frozen_string_literal: true + require 'rails_helper' -RSpec.describe Seat, type: :model do +RSpec.describe Seat do pending "add some examples to (or delete) #{__FILE__}" end diff --git a/spec/models/tables_arrangement_spec.rb b/spec/models/tables_arrangement_spec.rb index 4db0859..493318c 100644 --- a/spec/models/tables_arrangement_spec.rb +++ b/spec/models/tables_arrangement_spec.rb @@ -1,8 +1,12 @@ # Copyright (C) 2024 Manuel Bustillo +# Copyright (C) 2024 Manuel Bustillo + +# frozen_string_literal: true + require 'rails_helper' -RSpec.describe TablesArrangement, type: :model do +RSpec.describe TablesArrangement do describe 'callbacks' do it 'assigns a name before creation' do expect(create(:tables_arrangement).name).to be_present diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0277d58..ae1be79 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,7 +1,11 @@ # Copyright (C) 2024 Manuel Bustillo +# Copyright (C) 2024 Manuel Bustillo + +# frozen_string_literal: true + require 'rails_helper' -RSpec.describe User, type: :model do +RSpec.describe User do pending "add some examples to (or delete) #{__FILE__}" end diff --git a/spec/models/wedding_spec.rb b/spec/models/wedding_spec.rb index 6ec8a3e..84db4f1 100644 --- a/spec/models/wedding_spec.rb +++ b/spec/models/wedding_spec.rb @@ -1,22 +1,27 @@ # Copyright (C) 2024 Manuel Bustillo +# Copyright (C) 2024 Manuel Bustillo + +# frozen_string_literal: true + require 'rails_helper' -RSpec.describe Wedding, type: :model do +RSpec.describe Wedding do describe 'validations' do subject { build(:wedding) } - describe 'slug' do - it { should allow_value('foo').for(:slug) } - it { should allow_value('foo-bar').for(:slug) } - it { should allow_value('foo-123').for(:slug) } - it { should allow_value('foo-123-').for(:slug) } - it { should allow_value('foo--123').for(:slug) } - it { should_not allow_value('Foo').for(:slug) } - it { should_not allow_value('/foo').for(:slug) } - it { should_not allow_value('foo/123').for(:slug) } - it { should_not allow_value('foo_123').for(:slug) } - it { should_not allow_value('foo/').for(:slug) } + describe 'slug' do + it { is_expected.to allow_value('foo').for(:slug) } + it { is_expected.to allow_value('foo-bar').for(:slug) } + it { is_expected.to allow_value('foo-123').for(:slug) } + it { is_expected.to allow_value('foo-123-').for(:slug) } + it { is_expected.to allow_value('foo--123').for(:slug) } + + it { is_expected.not_to allow_value('Foo').for(:slug) } + it { is_expected.not_to allow_value('/foo').for(:slug) } + it { is_expected.not_to allow_value('foo/123').for(:slug) } + it { is_expected.not_to allow_value('foo_123').for(:slug) } + it { is_expected.not_to allow_value('foo/').for(:slug) } end end end diff --git a/spec/queries/expenses/total_query_spec.rb b/spec/queries/expenses/total_query_spec.rb index f721844..7847abe 100644 --- a/spec/queries/expenses/total_query_spec.rb +++ b/spec/queries/expenses/total_query_spec.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require 'rails_helper' module Expenses @@ -61,8 +63,8 @@ module Expenses end 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['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 diff --git a/spec/queries/groups/summary_query_spec.rb b/spec/queries/groups/summary_query_spec.rb index 6e2c3d2..1ef1a33 100644 --- a/spec/queries/groups/summary_query_spec.rb +++ b/spec/queries/groups/summary_query_spec.rb @@ -1,11 +1,13 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require 'rails_helper' module Groups RSpec.describe SummaryQuery do describe '#call' do - subject { described_class.new.call } + subject(:result) { described_class.new.call } context 'when there are no groups' do it { is_expected.to eq([]) } @@ -17,7 +19,7 @@ module Groups context 'when there are no guests' do it 'returns the summary of groups' do - is_expected.to contain_exactly( + expect(result).to contain_exactly( { 'id' => parent.id, 'name' => 'Friends', 'icon' => 'icon-1', @@ -58,11 +60,11 @@ module Groups create_list(:guest, 8, group: child, status: :invited) create_list(:guest, 9, group: child, status: :confirmed) create_list(:guest, 10, group: child, status: :declined) - create_list(:guest, 11, group: child, status: :tentative) + create_list(:guest, 11, group: child, status: :tentative) # rubocop:disable FactoryBot/ExcessiveCreateList end it 'returns the summary of groups' do - is_expected.to contain_exactly( + expect(result).to contain_exactly( { 'id' => parent.id, 'name' => 'Friends', diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index fa81db3..7058316 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,11 +1,13 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + # This file is copied to spec/ when you run 'rails generate rspec:install' require 'spec_helper' ENV['RAILS_ENV'] ||= 'test' require_relative '../config/environment' # 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' # Add additional requires below this line. Rails is not loaded until this point! diff --git a/spec/requests/affinities_spec.rb b/spec/requests/affinities_spec.rb new file mode 100644 index 0000000..c964018 --- /dev/null +++ b/spec/requests/affinities_spec.rb @@ -0,0 +1,52 @@ +# 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 diff --git a/spec/requests/captcha_spec.rb b/spec/requests/captcha_spec.rb index 607a2d9..17dc9bf 100644 --- a/spec/requests/captcha_spec.rb +++ b/spec/requests/captcha_spec.rb @@ -1,22 +1,23 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require 'swagger_helper' -RSpec.describe 'captcha', type: :request do +RSpec.describe 'captcha' do path '/captcha' do - post('create a CAPTCHA challenge') do tags 'CAPTCHA' consumes 'application/json' produces 'application/json' - + response(201, 'created') do schema type: :object, - required: %i[id], - properties: { - id: { type: :string, format: :uuid }, - media_url: { type: :string, format: :uri }, - } + required: %i[id], + properties: { + id: { type: :string, format: :uuid }, + media_url: { type: :string, format: :uri } + } xit end end diff --git a/spec/requests/expenses_spec.rb b/spec/requests/expenses_spec.rb index 068fec5..557f2e3 100644 --- a/spec/requests/expenses_spec.rb +++ b/spec/requests/expenses_spec.rb @@ -1,8 +1,10 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require 'swagger_helper' -RSpec.describe 'expenses', type: :request do +RSpec.describe 'expenses' do path '/{slug}/expenses' do get('list expenses') do tags 'Expenses' @@ -42,8 +44,8 @@ RSpec.describe 'expenses', type: :request do } } - response_empty_201 - response_422 + response_empty201 + response422 regular_api_responses end end @@ -60,9 +62,9 @@ RSpec.describe 'expenses', type: :request do properties: Swagger::Schema::EXPENSE } - response_empty_200 - response_422 - response_404 + response_empty200 + response422 + response404 regular_api_responses end @@ -71,8 +73,8 @@ RSpec.describe 'expenses', type: :request do produces 'application/json' parameter Swagger::Schema::SLUG parameter Swagger::Schema::ID - response_empty_200 - response_404 + response_empty200 + response404 regular_api_responses end end diff --git a/spec/requests/groups_spec.rb b/spec/requests/groups_spec.rb index bc6267b..30c512e 100644 --- a/spec/requests/groups_spec.rb +++ b/spec/requests/groups_spec.rb @@ -1,8 +1,10 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require 'swagger_helper' -RSpec.describe 'groups', type: :request do +RSpec.describe 'groups' do path '/{slug}/groups' do get('list groups') do tags 'Groups' @@ -10,29 +12,29 @@ 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 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 } - } - } - } + 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 @@ -99,8 +101,8 @@ RSpec.describe 'groups', type: :request do produces 'application/json' parameter Swagger::Schema::SLUG parameter name: :id, in: :path, type: :string, format: :uuid - - response_empty_200 + + response_empty200 regular_api_responses end end diff --git a/spec/requests/guests_spec.rb b/spec/requests/guests_spec.rb index bc73fbe..1ef80d4 100644 --- a/spec/requests/guests_spec.rb +++ b/spec/requests/guests_spec.rb @@ -1,8 +1,10 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require 'swagger_helper' -RSpec.describe 'guests', type: :request do +RSpec.describe 'guests' do path '/{slug}/guests' do get('list guests') do tags 'Guests' @@ -51,8 +53,8 @@ RSpec.describe 'guests', type: :request do } } - response_empty_201 - response_422 + response_empty201 + response422 regular_api_responses end end @@ -80,9 +82,9 @@ RSpec.describe 'guests', type: :request do } } - response_empty_200 - response_422 - response_404 + response_empty200 + response422 + response404 regular_api_responses end @@ -92,8 +94,8 @@ RSpec.describe 'guests', type: :request do parameter Swagger::Schema::SLUG parameter name: 'id', in: :path, type: :string, format: :uuid - response_empty_200 - response_404 + response_empty200 + response404 regular_api_responses end end diff --git a/spec/requests/schemas.rb b/spec/requests/schemas.rb index eb51216..ee914b3 100644 --- a/spec/requests/schemas.rb +++ b/spec/requests/schemas.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + module Swagger module Schema USER = { @@ -7,13 +9,13 @@ module Swagger email: { type: :string, format: :email }, created_at: SwaggerResponseHelper::TIMESTAMP, updated_at: SwaggerResponseHelper::TIMESTAMP - } + }.freeze - ID = { + ID = { # rubocop:disable Style/MutableConstant -- rswag modifies in: :path parameters name: 'id', in: :path, type: :string, - format: :uuid, + format: :uuid } GROUP = { @@ -21,18 +23,18 @@ module Swagger 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}$' } - } + }.freeze EXPENSE = { name: { type: :string }, amount: { type: :number, minimum: 0 }, pricing_type: { type: :string, enum: Expense.pricing_types.keys } - } + }.freeze - SLUG = { + SLUG = { # rubocop:disable Style/MutableConstant -- rswag modifies in: :path parameters name: 'slug', in: :path, - type: :string, + type: :string, pattern: Wedding::SLUG_REGEX, example: :default, description: 'Wedding slug' @@ -47,6 +49,6 @@ module Swagger answer: { type: :string } } } - } + }.freeze end -end \ No newline at end of file +end diff --git a/spec/requests/summary_spec.rb b/spec/requests/summary_spec.rb index 2b77892..f39ab7a 100644 --- a/spec/requests/summary_spec.rb +++ b/spec/requests/summary_spec.rb @@ -1,8 +1,10 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require 'swagger_helper' -RSpec.describe 'summary', type: :request do +RSpec.describe 'summary' do path '/{slug}/summary' do get('list summaries') do tags 'Summary' diff --git a/spec/requests/tables_arrangements_spec.rb b/spec/requests/tables_arrangements_spec.rb index c48c127..34c686f 100644 --- a/spec/requests/tables_arrangements_spec.rb +++ b/spec/requests/tables_arrangements_spec.rb @@ -1,8 +1,10 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require 'swagger_helper' -RSpec.describe 'tables_arrangements', type: :request do +RSpec.describe 'tables_arrangements' do path '/{slug}/tables_arrangements' do get('list tables arrangements') do tags 'Tables Arrangements' diff --git a/spec/requests/tokens_spec.rb b/spec/requests/tokens_spec.rb index 5a08e05..406e041 100644 --- a/spec/requests/tokens_spec.rb +++ b/spec/requests/tokens_spec.rb @@ -1,15 +1,5 @@ # Copyright (C) 2024 Manuel Bustillo -require 'swagger_helper' +# frozen_string_literal: true -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 +require 'swagger_helper' diff --git a/spec/requests/users/confirmations_spec.rb b/spec/requests/users/confirmations_spec.rb index 12bdfbc..8628683 100644 --- a/spec/requests/users/confirmations_spec.rb +++ b/spec/requests/users/confirmations_spec.rb @@ -1,9 +1,10 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require 'swagger_helper' -RSpec.describe 'users/confirmations', type: :request do - +RSpec.describe 'users/confirmations' do path '/{slug}/users/confirmation' do get('confirm user email') do tags 'Users' @@ -17,7 +18,7 @@ RSpec.describe 'users/confirmations', type: :request do xit end - response_422 + response422 end end end diff --git a/spec/requests/users/registrations_spec.rb b/spec/requests/users/registrations_spec.rb index 2cbaeea..f1dd82c 100644 --- a/spec/requests/users/registrations_spec.rb +++ b/spec/requests/users/registrations_spec.rb @@ -1,25 +1,26 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require 'swagger_helper' -RSpec.describe 'users/registrations', type: :request do - +RSpec.describe 'users/registrations' do path '/{slug}/users' do post('create registration') do tags 'Users Registrations' consumes 'application/json' produces 'application/json' - + parameter Swagger::Schema::SLUG parameter name: :body, in: :body, schema: { type: :object, - required: [:user, :wedding], + required: %i[user wedding], properties: { user: { type: :object, required: %i[email password password_confirmation], properties: { - email: { type: :string, format: :email}, + email: { type: :string, format: :email }, password: SwaggerResponseHelper::PASSWORD, password_confirmation: SwaggerResponseHelper::PASSWORD } diff --git a/spec/requests/users/sessions_spec.rb b/spec/requests/users/sessions_spec.rb index a803965..775f56c 100644 --- a/spec/requests/users/sessions_spec.rb +++ b/spec/requests/users/sessions_spec.rb @@ -1,11 +1,11 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require 'swagger_helper' -RSpec.describe 'users/sessions', type: :request do - +RSpec.describe 'users/sessions' do path '/{slug}/users/sign_in' do - post('create session') do tags 'Users Sessions' consumes 'application/json' @@ -32,7 +32,7 @@ RSpec.describe 'users/sessions', type: :request do xit end - response_401(message: 'Invalid Email or password.') + response401(message: 'Invalid Email or password.') end end diff --git a/spec/services/tables/discomfort_calculator_spec.rb b/spec/services/tables/discomfort_calculator_spec.rb index f5da2d1..3851bbf 100644 --- a/spec/services/tables/discomfort_calculator_spec.rb +++ b/spec/services/tables/discomfort_calculator_spec.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require 'rails_helper' module Tables RSpec.describe DiscomfortCalculator do @@ -12,8 +14,7 @@ module Tables describe '#calculate' do before do - allow(calculator).to receive(:table_size_penalty).and_return(2) - allow(calculator).to receive(:cohesion_discomfort).and_return(3) + allow(calculator).to receive_messages(table_size_penalty: 2, cohesion_discomfort: 3) end let(:table) { Table.new(create_list(:guest, 6)) } @@ -29,6 +30,7 @@ module Tables table.min_per_table = 5 table.max_per_table = 7 end + context 'when the number of guests is in the lower bound' do let(:table) { Table.new(create_list(:guest, 5)) } @@ -88,6 +90,7 @@ module Tables 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) end + context 'when the table contains just two guests' do context 'when they belong to the same group' do let(:table) { create_list(:guest, 2, group: family) } @@ -102,6 +105,7 @@ module Tables create(:guest, group: friends) ] end + it { expect(calculator.send(:cohesion_discomfort)).to eq(1) } end @@ -205,7 +209,7 @@ module Tables end 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 @@ -219,7 +223,7 @@ module Tables end 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 diff --git a/spec/services/tables/distribution_spec.rb b/spec/services/tables/distribution_spec.rb index df88a85..d22b817 100644 --- a/spec/services/tables/distribution_spec.rb +++ b/spec/services/tables/distribution_spec.rb @@ -1,23 +1,25 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require 'rails_helper' module Tables RSpec.describe Distribution do describe '#random_distribution' do - let(:subject) { described_class.new(min_per_table: 5, max_per_table: 10) } + subject(:distribution) { described_class.new(min_per_table: 5, max_per_table: 10) } context 'when there are fewer people than the minimum per table' do it 'creates one table' do - subject.random_distribution([1, 2, 3, 4]) - expect(subject.tables.count).to eq(1) + distribution.random_distribution([1, 2, 3, 4]) + expect(distribution.tables.count).to eq(1) end end context 'when there are more people than the maximum per table' do it 'creates multiple tables' do - subject.random_distribution([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) - expect(subject.tables.count).to be > 1 + distribution.random_distribution([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) + expect(distribution.tables.count).to be > 1 end end end diff --git a/spec/services/tables/shift_spec.rb b/spec/services/tables/shift_spec.rb index e4c48af..590d62b 100644 --- a/spec/services/tables/shift_spec.rb +++ b/spec/services/tables/shift_spec.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require 'rails_helper' module Tables @@ -7,7 +9,7 @@ module Tables describe '#each' do let(:shifts) do acc = [] - described_class.new(initial_solution).each do |solution| + described_class.new(initial_solution).each do |solution| # rubocop:disable Style/MapIntoArray -- #map is not implemented acc << solution.tables.map(&:dup) end acc diff --git a/spec/services/tables/swap_spec.rb b/spec/services/tables/swap_spec.rb index 159d998..94ccced 100644 --- a/spec/services/tables/swap_spec.rb +++ b/spec/services/tables/swap_spec.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require 'rails_helper' module Tables @@ -7,7 +9,7 @@ module Tables describe '#each' do let(:swaps) do acc = [] - described_class.new(initial_solution).each do |solution| + described_class.new(initial_solution).each do |solution| # rubocop:disable Style/MapIntoArray -- #map is not implemented acc << solution.tables.map(&:dup) end acc diff --git a/spec/services/vns/engine_spec.rb b/spec/services/vns/engine_spec.rb index d4064f6..e3f379c 100644 --- a/spec/services/vns/engine_spec.rb +++ b/spec/services/vns/engine_spec.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + require 'rails_helper' module VNS diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 25cfd37..2aa37f6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + # 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`. # The generated `.rspec` file contains `--require spec_helper` which will cause @@ -46,51 +48,49 @@ RSpec.configure do |config| # triggering implicit auto-inclusion in groups with matching metadata. config.shared_context_metadata_behavior = :apply_to_host_groups -# The settings below are suggested to provide a good initial experience -# with RSpec, but feel free to customize to your heart's content. -=begin - # This allows you to limit a spec run to individual examples or groups - # you care about by tagging them with `:focus` metadata. When nothing - # is tagged with `:focus`, all examples get run. RSpec also provides - # aliases for `it`, `describe`, and `context` that include `: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 - # 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: - # 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 - # individual spec file. - if config.files_to_run.one? - # Use the documentation formatter for detailed output, - # unless a formatter has already been configured - # (e.g. via a command-line flag). - 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 - # 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 - # the seed, which is printed after each run. - # --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 - # test failures related to randomization by passing the same `--seed` value - # as the one that triggered the failure. - Kernel.srand config.seed -=end + # The settings below are suggested to provide a good initial experience + # 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 + # # you care about by tagging them with `:focus` metadata. When nothing + # # is tagged with `:focus`, all examples get run. RSpec also provides + # # aliases for `it`, `describe`, and `context` that include `: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 + # # 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: + # # 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 + # # individual spec file. + # if config.files_to_run.one? + # # Use the documentation formatter for detailed output, + # # unless a formatter has already been configured + # # (e.g. via a command-line flag). + # 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 + # # 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 + # # the seed, which is printed after each run. + # # --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 + # # test failures related to randomization by passing the same `--seed` value + # # as the one that triggered the failure. + # Kernel.srand config.seed end diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb index 2b19be8..2c53057 100644 --- a/spec/swagger_helper.rb +++ b/spec/swagger_helper.rb @@ -3,10 +3,10 @@ # frozen_string_literal: true require 'rails_helper' -require_relative './swagger_response_helper' -require_relative './requests/schemas.rb' +require_relative 'swagger_response_helper' +require_relative 'requests/schemas' -include SwaggerResponseHelper +include SwaggerResponseHelper # rubocop:disable Style/MixinUsage RSpec.configure do |config| # Specify a root folder where Swagger JSON files are generated diff --git a/spec/swagger_response_helper.rb b/spec/swagger_response_helper.rb index d7f2e0e..b3f3e0b 100644 --- a/spec/swagger_response_helper.rb +++ b/spec/swagger_response_helper.rb @@ -1,18 +1,19 @@ # Copyright (C) 2024 Manuel Bustillo +# frozen_string_literal: true + module SwaggerResponseHelper 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 = {type: :string,pattern: TIMESTAMP_FORMAT,example: TIMESTAMP_EXAMPLE}.freeze - PASSWORD = { type: :string, minLength: User.password_length.begin, maxLength: User.password_length.end } - + TIMESTAMP = { type: :string, pattern: TIMESTAMP_FORMAT, example: TIMESTAMP_EXAMPLE }.freeze + PASSWORD = { type: :string, minLength: User.password_length.begin, maxLength: User.password_length.end }.freeze def regular_api_responses - response_401 + response401 end - def response_422 + def response422 response(422, 'Validation errors in input parameters') do produces 'application/json' error_schema @@ -20,7 +21,7 @@ module SwaggerResponseHelper end end - def response_empty_200 + def response_empty200 response(200, 'Success') do produces 'application/json' schema type: :object @@ -28,7 +29,7 @@ module SwaggerResponseHelper end end - def response_empty_201 + def response_empty201 response(201, 'Created') do produces 'application/json' schema type: :object @@ -36,7 +37,7 @@ module SwaggerResponseHelper end end - def response_404 + def response404 response(404, 'Record not found') do produces 'application/json' error_schema @@ -44,14 +45,14 @@ module SwaggerResponseHelper end end - def response_401(message: nil) + def response401(message: nil) response(401, 'Unauthorized') do produces 'application/json' schema type: :object, - required: %i[error], - properties: { - error: { type: :string, example: message || 'You need to sign in or sign up before continuing.' } - } + required: %i[error], + properties: { + error: { type: :string, example: message || 'You need to sign in or sign up before continuing.' } + } xit end end