diff --git a/Gemfile b/Gemfile index e7c867a..0a618dd 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,7 @@ gem 'rubytree' gem 'acts_as_tenant' gem 'httparty' gem 'rswag' +gem 'pluck_to_hash' group :development, :test do gem 'annotaterb' diff --git a/Gemfile.lock b/Gemfile.lock index dde22ed..854d1cd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -222,6 +222,9 @@ GEM ast (~> 2.4.1) racc pg (1.5.9) + pluck_to_hash (1.0.2) + activerecord (>= 4.0.2) + activesupport (>= 4.0.2) pry (0.15.0) coderay (~> 1.1) method_source (~> 1.0) @@ -413,6 +416,7 @@ DEPENDENCIES license_finder money pg (~> 1.1) + pluck_to_hash pry puma (>= 5.0) rack-cors diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 37df884..f04e394 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -4,4 +4,30 @@ class GroupsController < ApplicationController def index render json: Groups::SummaryQuery.new.call.as_json end + + def create + group = Group.create!(**group_params, parent:) + render json: group.as_json(only: %i[id name icon color parent_id]), status: :created + end + + def update + group = Group.find(params[:id]) + group.update!(**group_params, parent:) + render json: group.as_json(only: %i[id name icon color parent_id]), status: :ok + end + + def destroy + Group.find(params[:id]).destroy! + render json: {}, status: :ok + end + + private + + def parent + params[:group][:parent_id].present? ? Group.find(params[:group][:parent_id]) : nil + end + + def group_params + params.expect(group: [:name, :icon, :color]) + end end diff --git a/app/models/group.rb b/app/models/group.rb index 5056b28..a00a671 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -38,7 +38,7 @@ class Group < ApplicationRecord scope :roots, -> { where(parent_id: nil) } - has_many :guests + has_many :guests, dependent: :nullify def colorize_children(generation = 1) derived_colors = generation == 1 ? color.paint.palette.analogous(size: children.count) : color.paint.palette.decreasing_saturation diff --git a/app/models/guest.rb b/app/models/guest.rb index 929cc6e..a418a27 100644 --- a/app/models/guest.rb +++ b/app/models/guest.rb @@ -10,7 +10,7 @@ # status :integer default("considered") # created_at :datetime not null # updated_at :datetime not null -# group_id :uuid not null +# group_id :uuid # wedding_id :uuid not null # # Indexes diff --git a/app/queries/groups/summary_query.rb b/app/queries/groups/summary_query.rb index 70fc234..0736496 100644 --- a/app/queries/groups/summary_query.rb +++ b/app/queries/groups/summary_query.rb @@ -3,29 +3,19 @@ module Groups class SummaryQuery def call - ActiveRecord::Base.connection.execute(query).to_a - end - - private - - def query - <<~SQL.squish - SELECT - groups.id, - groups.name, - groups.icon, - groups.parent_id, - groups.color, - count(*) filter (where status IS NOT NULL) as total, - count(*) filter (where status = 0) as considered, - count(*) filter (where status = 10) as invited, - count(*) filter (where status = 20) as confirmed, - count(*) filter (where status = 30) as declined, - count(*) filter (where status = 40) as tentative - FROM groups - LEFT JOIN guests on groups.id = guests.group_id - GROUP BY groups.id - SQL + Group.left_joins(:guests).group(:id).pluck_to_hash( + :id, + :name, + :icon, + :parent_id, + :color, + Arel.sql('count(*) filter (where status IS NOT NULL) as total'), + Arel.sql('count(*) filter (where status = 0) as considered'), + Arel.sql('count(*) filter (where status = 10) as invited'), + Arel.sql('count(*) filter (where status = 20) as confirmed'), + Arel.sql('count(*) filter (where status = 30) as declined'), + Arel.sql('count(*) filter (where status = 40) as tentative'), + ) end end end diff --git a/config/routes.rb b/config/routes.rb index bd26594..5b2c031 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,7 +13,7 @@ Rails.application.routes.draw do get '/users/confirmation', to: 'users/confirmations#show', as: :confirmation end - resources :groups, only: :index + resources :groups, only: %i[index create update destroy] resources :guests, only: %i[index create update destroy] do post :bulk_update, on: :collection end diff --git a/db/migrate/20241208102932_allow_ungrouped_guests.rb b/db/migrate/20241208102932_allow_ungrouped_guests.rb new file mode 100644 index 0000000..c06d40c --- /dev/null +++ b/db/migrate/20241208102932_allow_ungrouped_guests.rb @@ -0,0 +1,7 @@ +# Copyright (C) 2024 Manuel Bustillo + +class AllowUngroupedGuests < ActiveRecord::Migration[8.0] + def change + change_column_null :guests, :group_id, true + end +end diff --git a/db/schema.rb b/db/schema.rb index 29940a3..0684e19 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_07_112305) do +ActiveRecord::Schema[8.0].define(version: 2024_12_08_102932) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -48,7 +48,7 @@ ActiveRecord::Schema[8.0].define(version: 2024_12_07_112305) do t.string "phone" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.uuid "group_id", null: false + t.uuid "group_id" t.integer "status", default: 0 t.string "name" t.uuid "wedding_id", null: false diff --git a/spec/requests/groups_spec.rb b/spec/requests/groups_spec.rb index 6d5f057..44f755b 100644 --- a/spec/requests/groups_spec.rb +++ b/spec/requests/groups_spec.rb @@ -31,5 +31,72 @@ RSpec.describe 'groups', type: :request do end regular_api_responses end + + post('create group') do + tags 'Groups' + consumes 'application/json' + produces 'application/json' + parameter Swagger::Schema::SLUG + parameter name: :body, in: :body, schema: { + type: :object, + required: %i[group], + properties: { + group: { + type: :object, + required: %i[name], + properties: Swagger::Schema::GROUP + } + } + } + response(201, 'created') do + schema type: :object, properties: { + id: { type: :string, format: :uuid, required: true }, + **Swagger::Schema::GROUP + } + + xit + end + regular_api_responses + end + + path '/{slug}/groups/{id}' do + put('update group') do + tags 'Groups' + consumes 'application/json' + produces 'application/json' + parameter Swagger::Schema::SLUG + parameter name: :id, in: :path, type: :string, format: :uuid + parameter name: :body, in: :body, schema: { + type: :object, + required: %i[group], + properties: { + group: { + type: :object, + required: %i[name], + properties: Swagger::Schema::GROUP + } + } + } + response(200, 'updated') do + schema type: :object, properties: { + id: { type: :string, format: :uuid, required: true }, + **Swagger::Schema::GROUP + } + + xit + end + regular_api_responses + end + + delete('delete group') do + tags 'Groups' + produces 'application/json' + parameter Swagger::Schema::SLUG + parameter name: :id, in: :path, type: :string, format: :uuid + + response_empty_200 + regular_api_responses + end + end end end diff --git a/spec/requests/schemas.rb b/spec/requests/schemas.rb index e1f7b43..e8e8e9b 100644 --- a/spec/requests/schemas.rb +++ b/spec/requests/schemas.rb @@ -4,10 +4,16 @@ module Swagger module Schema USER = { id: { type: :string, format: :uuid }, - email: { type: :string, format: :email }, - created_at: SwaggerResponseHelper::TIMESTAMP, - updated_at: SwaggerResponseHelper::TIMESTAMP - + email: { type: :string, format: :email }, + created_at: SwaggerResponseHelper::TIMESTAMP, + updated_at: SwaggerResponseHelper::TIMESTAMP + } + + GROUP = { + name: { type: :string }, + icon: { type: :string, example: 'pi pi-crown', description: 'The CSS classes used by the icon' }, + parent_id: { type: :string, format: :uuid }, + color: { type: :string, pattern: '^#(?:[0-9a-fA-F]{3}){1,2}$' } } SLUG = {