diff --git a/.rubocop.yml b/.rubocop.yml index c8b602f..d05c38d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,5 @@ -require: +plugins: - rubocop-performance - AllCops: TargetRubyVersion: 3.3.3 # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop diff --git a/Gemfile b/Gemfile index 23ee11f..6368a8b 100644 --- a/Gemfile +++ b/Gemfile @@ -9,6 +9,7 @@ gem 'decanter', '~> 4.0' gem 'factory_bot_rails', '~> 6.4' gem 'faker', '~> 3.3' gem 'figaro', '~> 1.2' +gem 'jbuilder' gem 'jsonapi-serializer', '~> 2.2' gem 'lp_token_auth', '~> 2.0' gem 'paper_trail', '~> 15.1' @@ -38,6 +39,7 @@ group :development, :test do gem 'pry', '~> 0.14.2' gem 'rspec-rails', '~> 6.0.0' + gem 'byebug' gem 'debug', platforms: %i[mri mingw x64_mingw] end diff --git a/Gemfile.lock b/Gemfile.lock index 58cff7d..5e7a3a5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -111,6 +111,7 @@ GEM bullet (7.2.0) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) + byebug (11.1.3) case_transform (0.2) activesupport childprocess (5.1.0) @@ -159,6 +160,9 @@ GEM pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) + jbuilder (2.13.0) + actionview (>= 5.0.0) + activesupport (>= 5.0.0) json (2.10.2) jsonapi-renderer (0.2.2) jsonapi-serializer (2.2.0) @@ -365,6 +369,7 @@ DEPENDENCIES binding_of_caller bootsnap bullet (~> 7.1) + byebug cssbundling-rails database_cleaner-active_record database_cleaner-redis @@ -373,6 +378,7 @@ DEPENDENCIES factory_bot_rails (~> 6.4) faker (~> 3.3) figaro (~> 1.2) + jbuilder jsonapi-serializer (~> 2.2) letter_opener lp_token_auth (~> 2.0) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4ac8823..3f1a602 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,2 +1,3 @@ class ApplicationController < ActionController::API + end diff --git a/app/controllers/attendees_controller.rb b/app/controllers/attendees_controller.rb new file mode 100644 index 0000000..84f537b --- /dev/null +++ b/app/controllers/attendees_controller.rb @@ -0,0 +1,23 @@ +class AttendeesController < ApplicationController + def create + @room = Room.find_by(invite_code: create_attendee_room_params[:invite_code], status: :open) + @attendee = Attendee.new(create_attendee_params.merge(room: @room)) + + if @attendee.save + render :create, status: :created + else + @errors = @attendee.errors.full_messages + render 'shared/errors', status: :unprocessable_entity + end + end + + def create_attendee_params + params.require(:attendee) + .permit(:name) + end + + def create_attendee_room_params + params.require(:room) + .permit(:invite_code) + end +end diff --git a/app/controllers/concerns/room_invite_codifier.rb b/app/controllers/concerns/room_invite_codifier.rb new file mode 100644 index 0000000..e179355 --- /dev/null +++ b/app/controllers/concerns/room_invite_codifier.rb @@ -0,0 +1,12 @@ +module RoomInviteCodifier + extend ActiveSupport::Concern + + def new_room_invite_code + code = nil + while code.nil? + code = SecureRandom.hex(3) # Generate a random 8-character hex string + # Check if the generated code already exists in the database + return code unless Room.where(invite_code: code, status: :open).any? + end + end +end diff --git a/app/controllers/rooms_controller.rb b/app/controllers/rooms_controller.rb new file mode 100644 index 0000000..3c12009 --- /dev/null +++ b/app/controllers/rooms_controller.rb @@ -0,0 +1,18 @@ +class RoomsController < ApplicationController + include RoomInviteCodifier + def create + @room = Room.new(create_room_params.merge(invite_code: new_room_invite_code, status: :open)) + if @room.save + render :create, status: :created + else + @errors = @room.errors.full_messages + render 'shared/errors', status: :unprocessable_entity + end + end + + def create_room_params + params + .require(:room) + .permit(:name, venue_ids: []) + end +end diff --git a/app/controllers/venues_controller.rb b/app/controllers/venues_controller.rb new file mode 100644 index 0000000..fa0f232 --- /dev/null +++ b/app/controllers/venues_controller.rb @@ -0,0 +1,5 @@ +class VenuesController < ApplicationController + def index + @venues = Venue.all + end +end diff --git a/app/models/attendee.rb b/app/models/attendee.rb new file mode 100644 index 0000000..049ffd8 --- /dev/null +++ b/app/models/attendee.rb @@ -0,0 +1,3 @@ +class Attendee < ApplicationRecord + belongs_to :room +end diff --git a/app/models/room.rb b/app/models/room.rb new file mode 100644 index 0000000..476db29 --- /dev/null +++ b/app/models/room.rb @@ -0,0 +1,8 @@ +class Room < ApplicationRecord + has_and_belongs_to_many :venues + has_many :attendees + + validates_presence_of :name + + enum status: { open: 0, closed: 1 } +end diff --git a/app/models/room_venue.rb b/app/models/room_venue.rb new file mode 100644 index 0000000..4de3703 --- /dev/null +++ b/app/models/room_venue.rb @@ -0,0 +1,6 @@ +class RoomVenue < ApplicationRecord + belongs_to :room + belongs_to :venue + + has_many :votes +end diff --git a/app/models/venue.rb b/app/models/venue.rb new file mode 100644 index 0000000..3f268d8 --- /dev/null +++ b/app/models/venue.rb @@ -0,0 +1,5 @@ +class Venue < ApplicationRecord + has_and_belongs_to_many :rooms + + enum category: { 'Tex-Mex': 0, Japanese: 1, Italian: 2, Mexican: 3 } +end diff --git a/app/models/vote.rb b/app/models/vote.rb new file mode 100644 index 0000000..4c58e4f --- /dev/null +++ b/app/models/vote.rb @@ -0,0 +1,2 @@ +class Vote < ApplicationRecord +end diff --git a/app/views/attendees/create.json.jbuilder b/app/views/attendees/create.json.jbuilder new file mode 100644 index 0000000..790a721 --- /dev/null +++ b/app/views/attendees/create.json.jbuilder @@ -0,0 +1,7 @@ +json.attendee do + json.id @attendee.id + json.room_name @attendee.room.name + json.attendees do + json.array! @attendee.room.attendees, :id, :name + end +end diff --git a/app/views/rooms/create.json.jbuilder b/app/views/rooms/create.json.jbuilder new file mode 100644 index 0000000..ab7724c --- /dev/null +++ b/app/views/rooms/create.json.jbuilder @@ -0,0 +1,6 @@ +json.room do + json.name @room.name + json.invite_code @room.invite_code + json.venue_ids @room.venues.map(&:id) + json.status @room.status +end diff --git a/app/views/shared/errors.json.jbuilder b/app/views/shared/errors.json.jbuilder new file mode 100644 index 0000000..0aba802 --- /dev/null +++ b/app/views/shared/errors.json.jbuilder @@ -0,0 +1,3 @@ +json.array! @errors do |error_message| + json.message error_message +end diff --git a/app/views/venues/index.json.jbuilder b/app/views/venues/index.json.jbuilder new file mode 100644 index 0000000..af9b519 --- /dev/null +++ b/app/views/venues/index.json.jbuilder @@ -0,0 +1,8 @@ +json.array! @venues do |venue| + json.name venue.name + json.instagram_link venue.instagram_link + json.menu_link venue.menu_link + json.cost venue.cost + json.rating venue.rating + json.category venue.category +end diff --git a/config/environments/development.rb b/config/environments/development.rb index a6a3e3e..0f23ad2 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -29,7 +29,7 @@ config.eager_load = false # Show full error reports. - config.consider_all_requests_local = true + # config.consider_all_requests_local = true # Enable server timing config.server_timing = true diff --git a/config/routes.rb b/config/routes.rb index bcd8db6..2da4c75 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,11 +1,9 @@ Rails.application.routes.draw do - - # GET route to know that API is online. - # Limited to json requests - sends a 200 response with empty headers and empty response body - if Rails.env.development? - get '/', to: proc { [200, {}, ['']] }, constraints: { format: 'json' } + constraints format: 'json' do + post '/rooms', to: 'rooms#create' + post 'rooms/:invite', to: 'rooms#join' + post '/venues', to: 'venues#create' + post '/attendees', to: 'attendees#create' + get '/venues', to: 'venues#index' end - - - end diff --git a/db/migrate/20250410174025_create_rooms_and_venues.rb b/db/migrate/20250410174025_create_rooms_and_venues.rb new file mode 100644 index 0000000..3e6e409 --- /dev/null +++ b/db/migrate/20250410174025_create_rooms_and_venues.rb @@ -0,0 +1,29 @@ +class CreateRoomsAndVenues < ActiveRecord::Migration[7.1] + def change + create_table :rooms do |t| + t.string :name + t.string :invite_code + t.integer :host_id + t.integer :status + + t.timestamps + end + + create_table :venues do |t| + t.string :name + t.string :instagram_link + t.string :menu_link + t.integer :cost + t.integer :rating + t.integer :category + + t.timestamps + end + + create_table :rooms_venues do |t| + # Intentionally keeping the id column to reference at vote time + t.belongs_to :room + t.belongs_to :venue + end + end +end diff --git a/db/migrate/20250410174408_create_attendees.rb b/db/migrate/20250410174408_create_attendees.rb new file mode 100644 index 0000000..f5db2ec --- /dev/null +++ b/db/migrate/20250410174408_create_attendees.rb @@ -0,0 +1,9 @@ +class CreateAttendees < ActiveRecord::Migration[7.1] + def change + create_table :attendees do |t| + t.references :room + t.string :name + t.timestamps + end + end +end diff --git a/db/migrate/20250410174650_create_votes.rb b/db/migrate/20250410174650_create_votes.rb new file mode 100644 index 0000000..321c865 --- /dev/null +++ b/db/migrate/20250410174650_create_votes.rb @@ -0,0 +1,9 @@ +class CreateVotes < ActiveRecord::Migration[7.1] + def change + create_table :votes do |t| + t.references :rooms_venues, null: false, foreign_key: true + t.references :attendee, null: false, foreign_key: true + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 6f9accf..5c33104 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,11 +10,46 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2025_04_10_155946) do +ActiveRecord::Schema[7.1].define(version: 2025_04_10_174650) do # These are extensions that must be enabled in order to support this database enable_extension "hstore" enable_extension "plpgsql" + create_table "attendees", force: :cascade do |t| + t.bigint "room_id" + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["room_id"], name: "index_attendees_on_room_id" + end + + create_table "rooms", force: :cascade do |t| + t.string "name" + t.string "invite_code" + t.integer "host_id" + t.integer "status" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "rooms_venues", force: :cascade do |t| + t.bigint "room_id" + t.bigint "venue_id" + t.index ["room_id"], name: "index_rooms_venues_on_room_id" + t.index ["venue_id"], name: "index_rooms_venues_on_venue_id" + end + + create_table "venues", force: :cascade do |t| + t.string "name" + t.string "instagram_link" + t.string "menu_link" + t.integer "cost" + t.integer "rating" + t.integer "category" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "versions", force: :cascade do |t| t.string "whodunnit" t.datetime "created_at" @@ -26,4 +61,15 @@ t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id" end + create_table "votes", force: :cascade do |t| + t.bigint "rooms_venues_id", null: false + t.bigint "attendee_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["attendee_id"], name: "index_votes_on_attendee_id" + t.index ["rooms_venues_id"], name: "index_votes_on_rooms_venues_id" + end + + add_foreign_key "votes", "attendees" + add_foreign_key "votes", "rooms_venues", column: "rooms_venues_id" end diff --git a/db/seeds.rb b/db/seeds.rb index 5715367..2541e65 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -7,4 +7,9 @@ # Character.create(name: "Luke", movie: movies.first) raise 'SEEDING NOT ALLOWED IN PRODUCTION' if Rails.env.production? -# Seeds::UserSeeder.new.run \ No newline at end of file + +100.times do +FactoryBot::create(:venue) +end + +# Seeds::UserSeeder.new.run diff --git a/lib/tasks/rebuild.rake b/lib/tasks/rebuild.rake new file mode 100644 index 0000000..5e569cc --- /dev/null +++ b/lib/tasks/rebuild.rake @@ -0,0 +1,14 @@ +namespace :db do + desc 'Drop, migrate and seed the database' + task rebuild: :environment do + puts 'Dropping database...' + Rake::Task['db:drop'].invoke + puts 'Creating database...' + Rake::Task['db:create'].invoke + puts 'Migrating database...' + Rake::Task['db:migrate'].invoke + puts 'Seeding database...' + Rake::Task['db:seed'].invoke + puts 'Database rebuilt successfully!' + end +end diff --git a/spec/factories/attendees.rb b/spec/factories/attendees.rb new file mode 100644 index 0000000..5dfbe37 --- /dev/null +++ b/spec/factories/attendees.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :attendee do + name { Faker::Name.name } + end +end diff --git a/spec/factories/room_venues.rb b/spec/factories/room_venues.rb new file mode 100644 index 0000000..658e17e --- /dev/null +++ b/spec/factories/room_venues.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :room_venue do + association :room + association :venue + end +end diff --git a/spec/factories/rooms.rb b/spec/factories/rooms.rb new file mode 100644 index 0000000..af87d5a --- /dev/null +++ b/spec/factories/rooms.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :room do + invite_code { SecureRandom.hex(10) } + venue { venue } + host_id { attendee } + end +end diff --git a/spec/factories/venues.rb b/spec/factories/venues.rb new file mode 100644 index 0000000..1addbf0 --- /dev/null +++ b/spec/factories/venues.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :venue do + name { Faker::Restaurant.name } + menu_link { 'http://google.com' } + cost { 100 } + rating { 5 } + category { Venue.categories.values.sample } + end +end diff --git a/spec/factories/votes.rb b/spec/factories/votes.rb new file mode 100644 index 0000000..11669eb --- /dev/null +++ b/spec/factories/votes.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :vote do + room_venue + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb deleted file mode 100644 index e005d5e..0000000 --- a/spec/rails_helper.rb +++ /dev/null @@ -1,70 +0,0 @@ -# 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? -require 'rspec/rails' -require 'database_cleaner-redis' -require 'database_cleaner-active_record' -require 'faker' -require 'shoulda/matchers' -# Add additional requires below this line. Rails is not loaded until this point! - -# Requires supporting ruby files with custom matchers and macros, etc, in -# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are -# run as spec files by default. This means that files in spec/support that end -# in _spec.rb will both be required and run as specs, causing the specs to be -# run twice. It is recommended that you do not name files matching this glob to -# end with _spec.rb. You can configure this pattern with the --pattern -# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. -# -# The following line is provided for convenience purposes. It has the downside -# of increasing the boot-up time by auto-requiring all files in the support -# directory. Alternatively, in the individual `*_spec.rb` files, manually -# require only the support files necessary. -# -# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } -Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } - -# Checks for pending migrations and applies them before tests are run. -# If you are not using ActiveRecord, you can remove these lines. -begin - ActiveRecord::Migration.maintain_test_schema! -rescue ActiveRecord::PendingMigrationError => e - abort e.to_s.strip -end -RSpec.configure do |config| - config.include FactoryBot::Syntax::Methods - - # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures - config.fixture_path = "#{Rails.root}/spec/fixtures" - - # If you're not using ActiveRecord, or you'd prefer not to run each of your - # examples within a transaction, remove the following line or assign false - # instead of true. - config.use_transactional_fixtures = true - - # You can uncomment this line to turn off ActiveRecord support entirely. - # config.use_active_record = false - - # RSpec Rails can automatically mix in different behaviours to your tests - # based on their file location, for example enabling you to call `get` and - # `post` in specs under `spec/controllers`. - # - # You can disable this behaviour by removing the line below, and instead - # explicitly tag your specs with their type, e.g.: - # - # RSpec.describe UsersController, type: :controller do - # # ... - # end - # - # The different available types are documented in the features, such as in - # https://rspec.info/features/6-0/rspec-rails - config.infer_spec_type_from_file_location! - - # Filter lines from Rails gems in backtraces. - config.filter_rails_from_backtrace! - # arbitrary gems may also be filtered via: - # config.filter_gems_from_backtrace("gem name") -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index 9c96a9b..0000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,92 +0,0 @@ -# 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 -# this file to always be loaded, without a need to explicitly require it in any -# files. -# -# Given that it is always loaded, you are encouraged to keep this file as -# light-weight as possible. Requiring heavyweight dependencies from this file -# will add to the boot time of your test suite on EVERY test run, even for an -# individual file that may not need all of that loaded. Instead, consider making -# a separate helper file that requires the additional dependencies and performs -# the additional setup, and require it from the spec files that actually need -# it. -# -# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration -RSpec.configure do |config| - # rspec-expectations config goes here. You can use an alternate - # assertion/expectation library such as wrong or the stdlib/minitest - # assertions if you prefer. - config.expect_with :rspec do |expectations| - # This option will default to `true` in RSpec 4. It makes the `description` - # and `failure_message` of custom matchers include text for helper methods - # defined using `chain`, e.g.: - # be_bigger_than(2).and_smaller_than(4).description - # # => "be bigger than 2 and smaller than 4" - # ...rather than: - # # => "be bigger than 2" - expectations.include_chain_clauses_in_custom_matcher_descriptions = true - end - - # rspec-mocks config goes here. You can use an alternate test double - # library (such as bogus or mocha) by changing the `mock_with` option here. - config.mock_with :rspec do |mocks| - # Prevents you from mocking or stubbing a method that does not exist on - # a real object. This is generally recommended, and will default to - # `true` in RSpec 4. - mocks.verify_partial_doubles = true - end - - # This option will default to `:apply_to_host_groups` in RSpec 4 (and will - # have no way to turn it off -- the option exists only for backwards - # compatibility in RSpec 3). It causes shared context metadata to be - # inherited by the metadata hash of host groups and examples, rather than - # 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. - # # 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/support/shoulda_matchers.rb b/spec/support/shoulda_matchers.rb deleted file mode 100644 index 7d045f3..0000000 --- a/spec/support/shoulda_matchers.rb +++ /dev/null @@ -1,6 +0,0 @@ -Shoulda::Matchers.configure do |config| - config.integrate do |with| - with.test_framework :rspec - with.library :rails - end -end