diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f7a5815 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,31 @@ +name: Build docker image +on: + push: + branches: + - main + pull_request: +jobs: + build-static-assets: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: bustikiller/wedding-planner:latest + cache-from: type=registry,ref=user/app:latest + cache-to: type=inline \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index eefea88..e4d4fec 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,3 +35,6 @@ jobs: env: RAILS_ENV: test DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres + - name: Clean up containers generated by this flow + if: failure() + run: docker ps --filter network=$JOB_CONTAINER_NAME-$GITHUB_JOB-network --filter name=$JOB_CONTAINER_NAME-* --format "{{.ID}}" | xargs docker rm -f diff --git a/Dockerfile b/Dockerfile index a885e56..80ee039 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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.2.0 +ARG RUBY_VERSION=3.3.4 FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base # Rails app lives here @@ -13,6 +13,7 @@ ENV RAILS_ENV="production" \ BUNDLE_PATH="/usr/local/bundle" \ BUNDLE_WITHOUT="development" +RUN apt-get update && apt-get install -y nodejs # Throw-away build stage to reduce size of final image FROM base as build diff --git a/Gemfile b/Gemfile index 3c61f52..6f53d2e 100644 --- a/Gemfile +++ b/Gemfile @@ -1,70 +1,35 @@ -source "https://rubygems.org" +source 'https://rubygems.org' -ruby "3.3.4" +ruby '3.3.4' +gem 'acts-as-taggable-on' +gem 'bootsnap', require: false +gem 'csv' +gem 'importmap-rails' +gem 'jbuilder' +gem 'money' +gem 'pg', '~> 1.1' +gem 'puma', '>= 5.0' +gem 'rails', '~> 7.1.3', '>= 7.1.3.2' +gem 'redis', '>= 4.0.1' +gem 'sprockets-rails' +gem 'stimulus-rails' +gem 'turbo-rails' +gem 'tzinfo-data', platforms: %i[windows jruby] -# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" -gem "rails", "~> 7.1.3", ">= 7.1.3.2" - -# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] -gem "sprockets-rails" - -# Use postgresql as the database for Active Record -gem "pg", "~> 1.1" - -# Use the Puma web server [https://github.com/puma/puma] -gem "puma", ">= 5.0" - -# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] -gem "importmap-rails" - -# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] -gem "turbo-rails" - -# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] -gem "stimulus-rails" - -# Build JSON APIs with ease [https://github.com/rails/jbuilder] -gem "jbuilder" - -# Use Redis adapter to run Action Cable in production -gem "redis", ">= 4.0.1" - -# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] -# gem "kredis" - -# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] -# gem "bcrypt", "~> 3.1.7" - -# Windows does not include zoneinfo files, so bundle the tzinfo-data gem -gem "tzinfo-data", platforms: %i[ windows jruby ] - -# Reduces boot times through caching; required in config/boot.rb -gem "bootsnap", require: false - -# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] -# gem "image_processing", "~> 1.2" +gem 'jsonapi-rails' +gem 'rack-cors' +gem 'react-rails' +gem 'rubytree' group :development, :test do - # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem - gem "debug", platforms: %i[ mri windows ] - gem 'rspec-rails', '~> 6.1.0' + gem 'debug', platforms: %i[mri windows] + gem 'factory_bot_rails' gem 'faker' gem 'pry' - gem "factory_bot_rails" + gem 'rspec-rails', '~> 6.1.0' end group :development do - # Use console on exceptions pages [https://github.com/rails/web-console] - gem "web-console" - - # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] - # gem "rack-mini-profiler" - - # Speed up commands on slow machines / big apps [https://github.com/rails/spring] - # gem "spring" + gem 'rubocop' + gem 'web-console' end - -gem "money" -gem 'acts-as-taggable-on' - -gem "rubytree" diff --git a/Gemfile.lock b/Gemfile.lock index 0e31210..8b9b054 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -77,16 +77,22 @@ GEM tzinfo (~> 2.0) acts-as-taggable-on (10.0.0) activerecord (>= 6.1, < 7.2) + ast (2.4.2) + babel-source (5.8.35) + babel-transpiler (0.7.0) + babel-source (>= 4.0, < 6) + execjs (~> 2.0) base64 (0.2.0) bigdecimal (3.1.8) bindex (0.8.1) - bootsnap (1.18.3) + bootsnap (1.18.4) msgpack (~> 1.2) builder (3.3.0) coderay (1.1.3) - concurrent-ruby (1.3.3) + concurrent-ruby (1.3.4) connection_pool (2.4.1) crass (1.0.6) + csv (3.3.0) date (3.3.4) debug (1.9.2) irb (~> 1.10) @@ -94,6 +100,7 @@ GEM diff-lcs (1.5.1) drb (2.2.1) erubi (1.13.0) + execjs (2.9.1) factory_bot (6.4.6) activesupport (>= 5.0.0) factory_bot_rails (6.4.3) @@ -117,6 +124,18 @@ GEM actionview (>= 5.0.0) activesupport (>= 5.0.0) json (2.7.2) + jsonapi-deserializable (0.2.0) + jsonapi-parser (0.1.1) + jsonapi-rails (0.4.1) + jsonapi-parser (~> 0.1.0) + jsonapi-rb (~> 0.5.0) + jsonapi-rb (0.5.0) + jsonapi-deserializable (~> 0.2.0) + jsonapi-serializable (~> 0.3.0) + jsonapi-renderer (0.2.2) + jsonapi-serializable (0.3.1) + jsonapi-renderer (~> 0.2.0) + language_server-protocol (3.17.0.3) loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -155,6 +174,10 @@ GEM racc (~> 1.4) nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) + parallel (1.26.2) + parser (3.3.4.2) + ast (~> 2.4.1) + racc pg (1.5.7) pry (0.14.2) coderay (~> 1.1) @@ -165,6 +188,8 @@ GEM nio4r (~> 2.0) racc (1.8.1) rack (3.1.7) + rack-cors (2.0.2) + rack (>= 2.0.0) rack-session (2.0.0) rack (>= 3.0.0) rack-test (2.1.0) @@ -201,15 +226,24 @@ GEM rake (>= 12.2) thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) + rainbow (3.1.1) rake (13.2.1) rdoc (6.7.0) psych (>= 4.0.0) + react-rails (3.2.1) + babel-transpiler (>= 0.7.0) + connection_pool + execjs + railties (>= 3.2) + tilt redis (5.2.0) redis-client (>= 0.22.0) redis-client (0.22.2) connection_pool + regexp_parser (2.9.2) reline (0.5.9) io-console (~> 0.5) + rexml (3.2.8) rspec-core (3.12.3) rspec-support (~> 3.12.0) rspec-expectations (3.12.4) @@ -227,6 +261,20 @@ GEM rspec-mocks (~> 3.12) rspec-support (~> 3.12) rspec-support (3.12.2) + rubocop (1.65.1) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.4, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.31.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.32.0) + parser (>= 3.3.1.0) + ruby-progressbar (1.13.0) rubytree (2.0.3) json (~> 2.0, > 2.3.1) sprockets (4.2.1) @@ -240,6 +288,7 @@ GEM railties (>= 6.0.0) stringio (3.1.1) thor (1.3.1) + tilt (2.4.0) timeout (0.4.1) turbo-rails (2.0.6) actionpack (>= 6.0.0) @@ -247,6 +296,7 @@ GEM railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + unicode-display_width (2.5.0) web-console (4.2.1) actionview (>= 6.0.0) activemodel (>= 6.0.0) @@ -269,18 +319,23 @@ PLATFORMS DEPENDENCIES acts-as-taggable-on bootsnap + csv debug factory_bot_rails faker importmap-rails jbuilder + jsonapi-rails money pg (~> 1.1) pry puma (>= 5.0) + rack-cors rails (~> 7.1.3, >= 7.1.3.2) + react-rails redis (>= 4.0.1) rspec-rails (~> 6.1.0) + rubocop rubytree sprockets-rails stimulus-rails diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb new file mode 100644 index 0000000..0f489fb --- /dev/null +++ b/app/controllers/groups_controller.rb @@ -0,0 +1,6 @@ +class GroupsController < ApplicationController + def index + roots = Group.where(parent_id: nil) + render jsonapi: roots, include: [children: [children: [:children]]] + end +end diff --git a/app/controllers/guests_controller.rb b/app/controllers/guests_controller.rb index 72baf50..925770f 100644 --- a/app/controllers/guests_controller.rb +++ b/app/controllers/guests_controller.rb @@ -6,9 +6,10 @@ class GuestsController < ApplicationController # GET /guests or /guests.json def index @guests = Guest.all - .left_outer_joins(:affinity_groups) - .order('tags.name' => :asc) - .includes(:affinity_groups, :unbreakable_bonds) + .joins(:group) + .order('groups.name' => :asc) + + render jsonapi: @guests end # GET /guests/1 or /guests/1.json diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb new file mode 100644 index 0000000..c091b2f --- /dev/null +++ b/app/helpers/groups_helper.rb @@ -0,0 +1,2 @@ +module GroupsHelper +end diff --git a/app/models/group.rb b/app/models/group.rb new file mode 100644 index 0000000..afcadaa --- /dev/null +++ b/app/models/group.rb @@ -0,0 +1,9 @@ +class Group < ApplicationRecord + validates :name, uniqueness: true + validates :name, :order, presence: true + + has_many :children, class_name: 'Group', foreign_key: 'parent_id' + belongs_to :parent, class_name: 'Group', optional: true + + has_many :guests +end diff --git a/app/models/guest.rb b/app/models/guest.rb index 60e3bc2..cc4f615 100644 --- a/app/models/guest.rb +++ b/app/models/guest.rb @@ -1,6 +1,14 @@ class Guest < ApplicationRecord acts_as_taggable_on :affinity_groups, :unbreakable_bonds - + belongs_to :group + + enum status: { + considered: 0, + invited: 10, + confirmed: 20, + declined: 30 + } + def full_name "#{first_name} #{last_name}" end diff --git a/app/serializers/serializable_group.rb b/app/serializers/serializable_group.rb new file mode 100644 index 0000000..f832475 --- /dev/null +++ b/app/serializers/serializable_group.rb @@ -0,0 +1,11 @@ +class SerializableGroup < JSONAPI::Serializable::Resource + type 'group' + + attributes :name, :icon + + has_many :children + + attribute :guest_count do + @object.guests.count + end +end diff --git a/app/serializers/serializable_guest.rb b/app/serializers/serializable_guest.rb new file mode 100644 index 0000000..647c106 --- /dev/null +++ b/app/serializers/serializable_guest.rb @@ -0,0 +1,17 @@ +class SerializableGuest < JSONAPI::Serializable::Resource + type 'guest' + + attributes :id, :email, :group_id, :status + + attribute :name do + "#{@object.first_name} #{@object.last_name}" + end + + attribute :group_name do + @object.group.name + end + + attribute :status do + @object.status.capitalize + end +end diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb new file mode 100644 index 0000000..a908920 --- /dev/null +++ b/config/initializers/cors.rb @@ -0,0 +1,8 @@ +# config/initializers/cors.rb + +Rails.application.config.middleware.insert_before 0, Rack::Cors do + allow do + origins '*' + resource '*', headers: :any, methods: [:get, :post, :patch, :put, :delete] + end + end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 67f091d..6e4d17b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ Rails.application.routes.draw do + resources :groups, only: :index resources :guests do post :import, on: :collection end diff --git a/db/migrate/20240811142121_create_groups.rb b/db/migrate/20240811142121_create_groups.rb new file mode 100644 index 0000000..0246978 --- /dev/null +++ b/db/migrate/20240811142121_create_groups.rb @@ -0,0 +1,13 @@ +class CreateGroups < ActiveRecord::Migration[7.1] + def change + create_table :groups, id: :uuid do |t| + t.string :name, null: false + t.string :icon + t.integer :order, null: false, default: 1 + + t.timestamps + end + + add_index :groups, :name, unique: true + end +end diff --git a/db/migrate/20240811143801_add_parent_to_group.rb b/db/migrate/20240811143801_add_parent_to_group.rb new file mode 100644 index 0000000..575af38 --- /dev/null +++ b/db/migrate/20240811143801_add_parent_to_group.rb @@ -0,0 +1,5 @@ +class AddParentToGroup < ActiveRecord::Migration[7.1] + def change + add_reference :groups, :parent, type: :uuid, index: true, foreign_key: { to_table: :groups } + end +end diff --git a/db/migrate/20240811154115_add_group_to_guest.rb b/db/migrate/20240811154115_add_group_to_guest.rb new file mode 100644 index 0000000..d4e2bd4 --- /dev/null +++ b/db/migrate/20240811154115_add_group_to_guest.rb @@ -0,0 +1,5 @@ +class AddGroupToGuest < ActiveRecord::Migration[7.1] + def change + add_reference :guests, :group, null: false, foreign_key: true, type: :uuid + end +end diff --git a/db/migrate/20240811170021_add_status_to_guest.rb b/db/migrate/20240811170021_add_status_to_guest.rb new file mode 100644 index 0000000..cd7be5b --- /dev/null +++ b/db/migrate/20240811170021_add_status_to_guest.rb @@ -0,0 +1,5 @@ +class AddStatusToGuest < ActiveRecord::Migration[7.1] + def change + add_column :guests, :status, :integer, default: 0 + end +end diff --git a/db/schema.rb b/db/schema.rb index 08c4026..c4145db 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_07_24_181853) do +ActiveRecord::Schema[7.1].define(version: 2024_08_11_170021) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -26,6 +26,17 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_24_181853) do t.datetime "updated_at", null: false end + create_table "groups", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "name", null: false + t.string "icon" + t.integer "order", default: 1, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.uuid "parent_id" + t.index ["name"], name: "index_groups_on_name", unique: true + t.index ["parent_id"], name: "index_groups_on_parent_id" + end + create_table "guests", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.string "first_name" t.string "last_name" @@ -33,6 +44,9 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_24_181853) do t.string "phone" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.uuid "group_id", null: false + t.integer "status", default: 0 + t.index ["group_id"], name: "index_guests_on_group_id" end create_table "seats", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -82,6 +96,8 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_24_181853) do t.index ["name"], name: "index_tags_on_name", unique: true end + add_foreign_key "groups", "groups", column: "parent_id" + add_foreign_key "guests", "groups" add_foreign_key "seats", "guests" add_foreign_key "seats", "tables_arrangements", on_delete: :cascade add_foreign_key "taggings", "tags" diff --git a/db/seeds.rb b/db/seeds.rb index 162a80f..391d6e1 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -5,6 +5,7 @@ Expense.delete_all Guest.delete_all ActsAsTaggableOn::Tagging.delete_all ActsAsTaggableOn::Tag.delete_all +Group.delete_all Expense.create!(name: 'Photographer', amount: 3000, pricing_type: 'fixed') Expense.create!(name: 'Country house', amount: 6000, pricing_type: 'fixed') @@ -21,53 +22,45 @@ Expense.create!(name: 'Transportation', amount: 3000, pricing_type: 'fixed') Expense.create!(name: 'Invitations', amount: 200, pricing_type: 'fixed') Expense.create!(name: 'Cake', amount: 500, pricing_type: 'fixed') - -samples = { - close_family_a: 10, - close_family_b: 10, - cousins_a: 20, - cousins_b: 15, - relatives_a: 15, - relatives_b: 10, - work_a: 10, - work_b: 10, - besties_work_a: 5, - besties_work_b: 5, - college_friends_a: 10, - college_friends_b: 10, - high_school_friends_a: 10, - high_school_friends_b: 10, - childhood_friends_a: 10, - childhood_friends_b: 10, - basket_team_a: 10, - football_team_a: 15, - dance_club: 10 -}.each_with_object([]) do |(affinity_group, count), acc| - count.times { acc << affinity_group } -end - -NUMBER_OF_GUESTS.times do - guest = Guest.create!(first_name: Faker::Name.first_name, - last_name: Faker::Name.last_name, - email: Faker::Internet.email, - phone: Faker::PhoneNumber.cell_phone) - - guest.affinity_group_list.add(samples.sample) - guest.save! -end - -# Add unbreakable bonds -Guest.affinity_group_counts.each do |group| - couples = (group.taggings_count / 4).floor - - guests_involved = Guest.tagged_with(group.name).limit(couples * 2) - guests_involved.each_slice(2) do |a, b| - bond_name = "#{a.full_name} & #{b.full_name}" - - a.unbreakable_bond_list.add(bond_name) - b.unbreakable_bond_list.add(bond_name) - - a.save! - b.save! +Group.create!(name: "Jim's guests", icon: 'pi pi-heart').tap do |parent| + parent.children.create!(name: "Jim's family", icon: 'pi pi-users').tap do |family| + family.children.create!(name: "Jim's close family", icon: 'pi pi-home') + family.children.create!(name: "Jim's cousins", icon: 'pi pi-home') + family.children.create!(name: "Jim's relatives", icon: 'pi pi-home') + end + parent.children.create!(name: "Jim's friends", icon: 'pi pi-bullseye') + parent.children.create!(name: "Jim's work", icon: 'pi pi-desktop').tap do |work| + work.children.create!(name: "Jim's besties at work", icon: 'pi pi-briefcase') end end + +Group.create!(name: "Pam's guests", icon: 'pi pi-heart-fill').tap do |parent| + parent.children.create!(name: "Pam's family", icon: 'pi pi-users').tap do |family| + family.children.create!(name: "Pam's close family", icon: 'pi pi-home') + family.children.create!(name: "Pam's cousins", icon: 'pi pi-home') + family.children.create!(name: "Pam's relatives", icon: 'pi pi-home') + end + parent.children.create!(name: "Pam's friends", icon: 'pi pi-bullseye') + parent.children.create!(name: "Pam's work", icon: 'pi pi-desktop').tap do |work| + work.children.create!(name: "Pam's besties at work", icon: 'pi pi-briefcase') + end +end + +Group.create!(name: 'Common guests', icon: 'pi pi-users').tap do |parent| + parent.children.create!(name: 'College friends', icon: 'pi pi-calculator') + parent.children.create!(name: 'High school friends', icon: 'pi pi-crown') + parent.children.create!(name: 'Childhood friends', icon: 'pi pi-envelope') +end + +groups = Group.all + +NUMBER_OF_GUESTS.times do + Guest.create!( + first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, + email: Faker::Internet.email, + phone: Faker::PhoneNumber.cell_phone, + group: groups.sample, + status: Guest.statuses.keys.sample + ) +end diff --git a/renovate.json b/renovate.json index 7190a60..aebcb03 100644 --- a/renovate.json +++ b/renovate.json @@ -1,3 +1,11 @@ { - "$schema": "https://docs.renovatebot.com/renovate-schema.json" -} + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "labels": ["dependencies"], + "packageRules": [ + { + "matchUpdateTypes": ["minor", "patch"], + "matchCurrentVersion": "!/^0/", + "automerge": true + } + ] +} \ No newline at end of file diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb new file mode 100644 index 0000000..9bfade0 --- /dev/null +++ b/spec/factories/groups.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :group do + sequence(:name) { |i| "Group #{i}" } + order { 1 } + end +end diff --git a/spec/factories/guest.rb b/spec/factories/guest.rb index f761dd9..7ff9066 100644 --- a/spec/factories/guest.rb +++ b/spec/factories/guest.rb @@ -1,5 +1,7 @@ FactoryBot.define do factory :guest do + association :group + first_name { Faker::Name.first_name } last_name { Faker::Name.last_name } email { Faker::Internet.email } diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb new file mode 100644 index 0000000..be100cf --- /dev/null +++ b/spec/models/group_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Group, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end