Compare commits

..

No commits in common. "5f419c64e39f7c91c349c1e18a650ea82c5a342f" and "c37713af8fbfe07a317c9f1e14ff42ddd5b87051" have entirely different histories.

21 changed files with 57 additions and 436 deletions

3
.gitignore vendored
View File

@ -33,6 +33,3 @@
# Ignore master key for decrypting credentials and more.
/config/master.key
# Ignore swagger generated documentation
swagger/v1/swagger.yaml

View File

@ -1,7 +1,7 @@
# syntax = docker/dockerfile:1
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
ARG RUBY_VERSION=3.3.6
ARG RUBY_VERSION=3.3.5
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base
# Rails app lives here

View File

@ -28,7 +28,6 @@ group :development, :test do
gem 'license_finder'
gem 'pry'
gem 'rspec-rails', '~> 7.1.0'
gem 'rswag'
gem 'shoulda-matchers', '~> 6.0'
end

View File

@ -72,8 +72,6 @@ GEM
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
annotaterb (4.13.0)
ast (2.4.2)
babel-source (5.8.35)
@ -129,8 +127,6 @@ GEM
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
json (2.7.5)
json-schema (5.0.1)
addressable (~> 2.8)
jsonapi-deserializable (0.2.0)
jsonapi-parser (0.1.1)
jsonapi-rails (0.4.1)
@ -161,9 +157,9 @@ GEM
net-pop
net-smtp
marcel (1.0.4)
method_source (1.1.0)
method_source (1.0.0)
mini_mime (1.1.5)
minitest (5.25.2)
minitest (5.25.1)
money (6.19.0)
i18n (>= 0.6.4, <= 2)
msgpack (1.7.2)
@ -194,12 +190,11 @@ GEM
ast (~> 2.4.1)
racc
pg (1.5.9)
pry (0.15.0)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
psych (5.2.0)
stringio
public_suffix (6.0.1)
puma (6.4.3)
nio4r (~> 2.0)
raabro (1.4.0)
@ -277,21 +272,6 @@ GEM
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.1)
rswag (2.16.0)
rswag-api (= 2.16.0)
rswag-specs (= 2.16.0)
rswag-ui (= 2.16.0)
rswag-api (2.16.0)
activesupport (>= 5.2, < 8.1)
railties (>= 5.2, < 8.1)
rswag-specs (2.16.0)
activesupport (>= 5.2, < 8.1)
json-schema (>= 2.2, < 6.0)
railties (>= 5.2, < 8.1)
rspec-core (>= 2.14)
rswag-ui (2.16.0)
actionpack (>= 5.2, < 8.1)
railties (>= 5.2, < 8.1)
rubocop (1.68.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
@ -311,7 +291,7 @@ GEM
securerandom (0.3.2)
shoulda-matchers (6.4.0)
activesupport (>= 5.2.0)
solid_queue (1.0.2)
solid_queue (1.0.1)
activejob (>= 7.1)
activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1)
@ -382,7 +362,6 @@ DEPENDENCIES
react-rails
redis (>= 4.0.1)
rspec-rails (~> 7.1.0)
rswag
rubocop
rubytree
shoulda-matchers (~> 6.0)

View File

@ -42,7 +42,8 @@ projects <or anything else>
Docker compose is the recommended way to run Libre Wedding Planner for development purposes. After downloading both repositories, `cd` to the root of `wedding-planner` and run:
```bash
docker compose up --build
docker compose build
docker compose up
```
Several containers will be started:
@ -67,16 +68,6 @@ Unit tests can be executed with
bundle exec rspec
```
## API documentation
Generate the OpenAPI documentation with the command:
```
rake rswag:specs:swaggerize
```
The documentation is available in Swagger UI in http://libre-wedding-planner.app.localhost/api/api-docs/index.html. If testing the API through the UI, you will need to select the second server (which includes the `/api` path), intended for development.
## Contributing
Contributions of all kinds (code, UX/UI, testing, translations, etc.) are welcome. The procedures to contribute are still being defined, but don't hesitate to reach out in case you want to participate.

View File

@ -3,41 +3,13 @@
class ApplicationController < ActionController::Base
after_action :set_csrf_cookie
skip_before_action :verify_authenticity_token, if: :development_swagger?
rescue_from ActiveRecord::RecordInvalid do |exception|
render json: {
message: 'Record invalid',
errors: exception.record.errors.full_messages
}, status: :unprocessable_entity
end
rescue_from ActionController::ParameterMissing do |exception|
render json: {
message: 'Parameter missing',
errors: [exception.message]
}, status: :bad_request
end
rescue_from ActiveRecord::RecordNotFound do |exception|
render json: {
message: 'Record not found',
errors: [exception.message]
}, status: :not_found
end
private
def development_swagger?
Rails.env.test? ||
Rails.env.development? && request.headers['referer'].include?('/api-docs/index.html')
end
def set_csrf_cookie
cookies['csrf-token'] = {
cookies["csrf-token"] = {
value: form_authenticity_token,
secure: Rails.env.production?,
same_site: :strict
same_site: :strict,
}
end
end

View File

@ -4,30 +4,20 @@ require 'csv'
class GuestsController < ApplicationController
def index
render json: Guest.all.includes(:group)
@guests = Guest.all.includes(:group)
.joins(:group)
.order('groups.name' => :asc, name: :asc)
.as_json(only: %i[id name status], include: { group: { only: %i[id name] } })
end
def create
Guest.create!(guest_params)
render json: {}, status: :created
render jsonapi: @guests
end
def update
Guest.find(params[:id]).update!(guest_params)
Guests::UpdateUseCase.new(guest_ids: [params[:id]], params: params.require(:guest).permit(:name)).call
render json: {}, status: :ok
end
def destroy
Guest.find(params[:id]).destroy!
def bulk_update
Guests::UpdateUseCase.new(guest_ids: params[:guest_ids], params: params.require(:properties).permit(:status)).call
render json: {}, status: :ok
end
private
def guest_params
params.require(:guest).permit(:name, :group_id, :status)
end
end

View File

@ -12,11 +12,4 @@
# updated_at :datetime not null
#
class Expense < ApplicationRecord
enum :pricing_type,
fixed: 'fixed',
per_person: 'per_person'
validates :name, presence: true
validates :amount, presence: true, numericality: { greater_than: 0 }
validates :pricing_type, presence: true
end

View File

@ -29,22 +29,7 @@ class Guest < ApplicationRecord
confirmed: 20,
declined: 30,
tentative: 40
}, validate: true
validates :name, presence: true
}
scope :potential, -> { where.not(status: %i[declined considered]) }
after_save :recalculate_simulations, if: :saved_change_to_status?
after_destroy :recalculate_simulations
has_many :seats, dependent: :delete_all
private
def recalculate_simulations
TablesArrangement.delete_all
ActiveJob.perform_all_later(50.times.map { TableSimulatorJob.new })
end
end

View File

@ -10,7 +10,7 @@ module Groups
def query
<<~SQL.squish
SELECT
SELECT#{' '}
groups.id,
groups.name,
groups.icon,

View File

@ -0,0 +1,22 @@
# Copyright (C) 2024 Manuel Bustillo
module Guests
class UpdateUseCase
private attr_reader :guest_ids, :params
def initialize(guest_ids:, params:)
@guest_ids = guest_ids
@params = params
end
def call
Guest.where(id: guest_ids).update!(params)
# TODO: Not all status transitions may require a table re-arrangement
return unless params.key?(:status)
TablesArrangement.delete_all
ActiveJob.perform_all_later(50.times.map { TableSimulatorJob.new })
end
end
end

View File

@ -1,16 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
Rswag::Api.configure do |c|
# Specify a root folder where Swagger JSON files are located
# This is used by the Swagger middleware to serve requests for API descriptions
# NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure
# that it's configured to generate files in the same folder
c.openapi_root = Rails.root.to_s + '/swagger'
# Inject a lambda function to alter the returned Swagger prior to serialization
# The function will have access to the rack env for the current request
# For example, you could leverage this to dynamically assign the "host" property
#
#c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] }
end

View File

@ -1,18 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
Rswag::Ui.configure do |c|
# List the Swagger endpoints that you want to be documented through the
# swagger-ui. The first parameter is the path (absolute or relative to the UI
# host) to the corresponding endpoint and the second is a title that will be
# displayed in the document selector.
# NOTE: If you're using rspec-api to expose Swagger files
# (under openapi_root) as JSON or YAML endpoints, then the list below should
# correspond to the relative paths for those endpoints.
c.swagger_endpoint '/api/api-docs/v1/swagger.yaml', 'API V1 Docs'
# Add Basic Auth in case your API is private
# c.basic_auth_enabled = true
# c.basic_auth_credentials 'username', 'password'
end

View File

@ -1,10 +1,8 @@
# Copyright (C) 2024 Manuel Bustillo
Rails.application.routes.draw do
mount Rswag::Ui::Engine => '/api-docs'
mount Rswag::Api::Engine => '/api-docs'
resources :groups, only: :index
resources :guests, only: %i[index create update destroy] do
resources :guests, only: %i[index update] do
post :bulk_update, on: :collection
end
resources :expenses, only: %i[index update] do

View File

@ -3,10 +3,5 @@
require 'rails_helper'
RSpec.describe Expense, type: :model do
describe 'validations' do
it { should validate_presence_of(:name) }
it { should validate_presence_of(:amount) }
it { should validate_numericality_of(:amount).is_greater_than(0) }
it { should validate_presence_of(:pricing_type) }
end
pending "add some examples to (or delete) #{__FILE__}"
end

View File

@ -3,8 +3,6 @@
require 'rails_helper'
RSpec.describe Guest, type: :model do
describe 'validations' do
it { should validate_presence_of(:name) }
it do
should define_enum_for(:status).with_values(
considered: 0,
@ -14,7 +12,6 @@ RSpec.describe Guest, type: :model do
tentative: 40
)
end
end
it { should belong_to(:group) }

View File

@ -1,49 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
require 'swagger_helper'
RSpec.describe 'expenses', type: :request do
path '/expenses' do
get('list expenses') do
tags 'Expenses'
produces 'application/json'
response(200, 'successful') do
schema type: :array,
items: {
type: :object,
required: %i[id name amount pricing_type],
properties: {
id: { type: :string, format: :uuid },
name: { type: :string },
amount: { type: :number },
pricing_type: { type: :string, enum: Expense.pricing_types.keys }
}
}
xit
end
end
end
path '/expenses/{id}' do
patch('update expense') do
tags 'Expenses'
consumes 'application/json'
produces 'application/json'
parameter name: 'id', in: :path, type: :string, format: :uuid, description: 'id'
parameter name: :body, in: :body, schema: {
type: :object,
properties: {
name: { type: :string },
amount: { type: :number, minimum: 0 },
pricing_type: { type: :string, enum: Expense.pricing_types.keys }
}
}
response_empty_200
response_422
response_404
end
end
end

View File

@ -1,33 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
require 'swagger_helper'
RSpec.describe 'groups', type: :request do
path '/groups' do
get('list groups') do
tags 'Groups'
produces 'application/json'
response(200, 'successful') do
schema type: :array,
items: {
type: :object,
required: %i[id name icon parent_id color total considered invited confirmed declined tentative],
properties: {
id: { type: :string, format: :uuid, required: true },
name: { type: :string },
icon: { type: :string, example: 'pi pi-crown', description: 'The CSS classes used by the icon' },
parent_id: { type: :string, format: :uuid },
color: { type: :string, pattern: '^#(?:[0-9a-fA-F]{3}){1,2}$' },
total: { type: :integer, minimum: 0, description: 'Total number of guests in any status' },
considered: { type: :integer, minimum: 0 },
invited: { type: :integer, minimum: 0 },
confirmed: { type: :integer, minimum: 0 },
declined: { type: :integer, minimum: 0 },
tentative: { type: :integer, minimum: 0 }
}
}
xit
end
end
end
end

View File

@ -1,91 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
require 'swagger_helper'
RSpec.describe 'guests', type: :request do
path '/guests' do
get('list guests') do
tags 'Guests'
produces 'application/json'
response(200, 'successful') do
schema type: :array,
items: {
type: :object,
required: %i[id name status group],
properties: {
id: { type: :string, format: :uuid },
name: { type: :string },
status: { type: :string, enum: Guest.statuses.keys },
group: { type: :object,
required: %i[id name],
properties: {
id: { type: :string, format: :uuid },
name: { type: :string }
} }
}
}
xit
end
end
post('create guest') do
tags 'Guests'
consumes 'application/json'
produces 'application/json'
parameter name: :body, in: :body, schema: {
type: :object,
required: %i[guest],
properties: {
guest: {
type: :object,
required: %i[name group_id status],
properties: {
name: { type: :string },
group_id: { type: :string, format: :uuid },
status: { type: :string, enum: Guest.statuses.keys }
}
}
}
}
response_empty_201
response_422
end
end
path '/guests/{id}' do
patch('update guest') do
tags 'Guests'
consumes 'application/json'
produces 'application/json'
parameter name: 'id', in: :path, type: :string, format: :uuid
parameter name: :body, in: :body, schema: {
type: :object,
required: %i[guest],
properties: {
guest: {
type: :object,
properties: {
name: { type: :string },
group_id: { type: :string, format: :uuid },
status: { type: :string, enum: Guest.statuses.keys }
}
}
}
}
response_empty_200
response_422
response_404
end
delete('delete guest') do
tags 'Guests'
produces 'application/json'
parameter name: 'id', in: :path, type: :string, format: :uuid
response_empty_200
response_404
end
end
end

View File

@ -1,44 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
# frozen_string_literal: true
require 'rails_helper'
require_relative './swagger_response_helper'
include SwaggerResponseHelper
RSpec.configure do |config|
# Specify a root folder where Swagger JSON files are generated
# NOTE: If you're using the rswag-api to serve API descriptions, you'll need
# to ensure that it's configured to serve Swagger from the same folder
config.openapi_root = Rails.root.join('swagger').to_s
# Define one or more Swagger documents and provide global metadata for each one
# When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will
# be generated at the provided relative path under openapi_root
# By default, the operations defined in spec files are added to the first
# document below. You can override this behavior by adding a openapi_spec tag to the
# the root example_group in your specs, e.g. describe '...', openapi_spec: 'v2/swagger.json'
config.openapi_specs = {
'v1/swagger.yaml' => {
openapi: '3.0.1',
info: {
title: 'API V1',
version: 'v1'
},
paths: {},
servers: [
{
url: 'http://libre-wedding-planner.app.localhost/api',
description: 'Suitable for development'
}
]
}
}
# Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'.
# The openapi_specs configuration option has the filename including format in
# the key, this may want to be changed to avoid putting yaml in json files.
# Defaults to json. Accepts ':json' and ':yaml'.
config.openapi_format = :yaml
end

View File

@ -1,46 +0,0 @@
# Copyright (C) 2024 Manuel Bustillo
module SwaggerResponseHelper
def response_422
response(422, 'Validation errors in input parameters') do
produces 'application/json'
error_schema
xit
end
end
def response_empty_200
response(200, 'Success') do
produces 'application/json'
schema type: :object
xit
end
end
def response_empty_201
response(201, 'Created') do
produces 'application/json'
schema type: :object
xit
end
end
def response_404
response(404, 'Record not found') do
produces 'application/json'
error_schema
xit
end
end
private
def error_schema
schema type: :object,
required: %i[message errors],
properties: {
message: { type: :string },
errors: { type: :array, items: { type: :string } }
}
end
end