diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..7f5a804c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,42 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. + +# Ignore git directory. +/.git/ +/.gitignore + +# Ignore bundler config. +/.bundle + +# Ignore all environment files (except templates). +/.env* +!/.env*.erb + +# Ignore all default key files. +/config/master.key +/config/credentials/*.key + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/.keep + +# Ignore CI service files. +/.github + +# Ignore development files +/.devcontainer + +# Ignore Docker-related files +/.dockerignore +/Dockerfile* diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..8dc43234 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored +config/credentials/*.yml.enc diff=rails_credentials +config/credentials.yml.enc diff=rails_credentials diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..f0527e6b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: +- package-ecosystem: bundler + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..00af91f6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: CI + +on: + pull_request: + push: + branches: [ main ] + +jobs: + scan_ruby: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Scan for common Rails security vulnerabilities using static analysis + run: bin/brakeman --no-pager + + scan_js: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Scan for security vulnerabilities in JavaScript dependencies + run: bin/importmap audit + + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Lint code for consistent style + run: bin/rubocop -f github + + test: + runs-on: ubuntu-latest + + # services: + # redis: + # image: redis + # ports: + # - 6379:6379 + # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - name: Install packages + run: sudo apt-get update && sudo apt-get install --no-install-recommends -y google-chrome-stable curl libjemalloc2 libvips sqlite3 + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Run tests + env: + RAILS_ENV: test + # REDIS_URL: redis://localhost:6379/0 + run: bin/rails db:test:prepare test test:system + + - name: Keep screenshots from failed system tests + uses: actions/upload-artifact@v4 + if: failure() + with: + name: screenshots + path: ${{ github.workspace }}/tmp/screenshots + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..5ba3862b --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# Temporary files generated by your text editor or operating system +# belong in git's global ignore instead: +# `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore` + +# Ignore bundler config. +/.bundle + +# Ignore all environment files (except templates). +/.env* +!/.env*.erb + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +# Ignore master key for decrypting credentials and more. +/config/master.key diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..f9d86d4a --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,8 @@ +# Omakase Ruby styling for Rails +inherit_gem: { rubocop-rails-omakase: rubocop.yml } + +# Overwrite or add rules to create your own house style +# +# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` +# Layout/SpaceInsideArrayLiteralBrackets: +# Enabled: false diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..6d5369b9 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-3.3.4 diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..db688ce7 --- /dev/null +++ b/Gemfile @@ -0,0 +1,43 @@ +source "https://rubygems.org" + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "~> 7.2.1" +# Use sqlite3 as the database for Active Record +gem "sqlite3", ">= 1.4" +# Use the Puma web server [https://github.com/puma/puma] +gem "puma", ">= 5.0" +# 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" + +# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin Ajax possible +# gem "rack-cors" + +group :development, :test do + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem + gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" + + # Static analysis for security vulnerabilities [https://brakemanscanner.org/] + gem "brakeman", require: false + + # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] + gem "rubocop-rails-omakase", require: false +end + + diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..b72b6186 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,269 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (7.2.2) + actionpack (= 7.2.2) + activesupport (= 7.2.2) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (7.2.2) + actionpack (= 7.2.2) + activejob (= 7.2.2) + activerecord (= 7.2.2) + activestorage (= 7.2.2) + activesupport (= 7.2.2) + mail (>= 2.8.0) + actionmailer (7.2.2) + actionpack (= 7.2.2) + actionview (= 7.2.2) + activejob (= 7.2.2) + activesupport (= 7.2.2) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (7.2.2) + actionview (= 7.2.2) + activesupport (= 7.2.2) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4, < 3.2) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (7.2.2) + actionpack (= 7.2.2) + activerecord (= 7.2.2) + activestorage (= 7.2.2) + activesupport (= 7.2.2) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.2.2) + activesupport (= 7.2.2) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.2.2) + activesupport (= 7.2.2) + globalid (>= 0.3.6) + activemodel (7.2.2) + activesupport (= 7.2.2) + activerecord (7.2.2) + activemodel (= 7.2.2) + activesupport (= 7.2.2) + timeout (>= 0.4.0) + activestorage (7.2.2) + actionpack (= 7.2.2) + activejob (= 7.2.2) + activerecord (= 7.2.2) + activesupport (= 7.2.2) + marcel (~> 1.0) + activesupport (7.2.2) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + ast (2.4.2) + base64 (0.2.0) + benchmark (0.3.0) + bigdecimal (3.1.8) + bootsnap (1.18.4) + msgpack (~> 1.2) + brakeman (6.2.2) + racc + builder (3.3.0) + concurrent-ruby (1.3.4) + connection_pool (2.4.1) + crass (1.0.6) + date (3.4.0) + debug (1.9.2) + irb (~> 1.10) + reline (>= 0.3.8) + drb (2.2.1) + erubi (1.13.0) + globalid (1.2.1) + activesupport (>= 6.1) + i18n (1.14.6) + concurrent-ruby (~> 1.0) + io-console (0.7.2) + irb (1.14.1) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + json (2.8.1) + language_server-protocol (3.17.0.3) + logger (1.6.1) + loofah (2.23.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.0.4) + mini_mime (1.1.5) + minitest (5.25.1) + msgpack (1.7.3) + net-imap (0.5.0) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.0) + net-protocol + nio4r (2.7.4) + nokogiri (1.16.7-aarch64-linux) + racc (~> 1.4) + nokogiri (1.16.7-arm-linux) + racc (~> 1.4) + nokogiri (1.16.7-arm64-darwin) + racc (~> 1.4) + nokogiri (1.16.7-x86-linux) + racc (~> 1.4) + nokogiri (1.16.7-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.16.7-x86_64-linux) + racc (~> 1.4) + parallel (1.26.3) + parser (3.3.6.0) + ast (~> 2.4.1) + racc + psych (5.2.0) + stringio + puma (6.4.3) + nio4r (~> 2.0) + racc (1.8.1) + rack (3.1.8) + rack-session (2.0.0) + rack (>= 3.0.0) + rack-test (2.1.0) + rack (>= 1.3) + rackup (2.2.0) + rack (>= 3) + rails (7.2.2) + actioncable (= 7.2.2) + actionmailbox (= 7.2.2) + actionmailer (= 7.2.2) + actionpack (= 7.2.2) + actiontext (= 7.2.2) + actionview (= 7.2.2) + activejob (= 7.2.2) + activemodel (= 7.2.2) + activerecord (= 7.2.2) + activestorage (= 7.2.2) + activesupport (= 7.2.2) + bundler (>= 1.15.0) + railties (= 7.2.2) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.2.2) + actionpack (= 7.2.2) + activesupport (= 7.2.2) + irb (~> 1.13) + rackup (>= 1.0.0) + 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) + regexp_parser (2.9.2) + reline (0.5.10) + io-console (~> 0.5) + rubocop (1.68.0) + 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) + rubocop-ast (>= 1.32.2, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.34.0) + parser (>= 3.3.1.0) + rubocop-minitest (0.36.0) + rubocop (>= 1.61, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-performance (1.22.1) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rails (2.27.0) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.52.0, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rails-omakase (1.0.0) + rubocop + rubocop-minitest + rubocop-performance + rubocop-rails + ruby-progressbar (1.13.0) + securerandom (0.3.1) + sqlite3 (2.2.0-aarch64-linux-gnu) + sqlite3 (2.2.0-aarch64-linux-musl) + sqlite3 (2.2.0-arm-linux-gnu) + sqlite3 (2.2.0-arm-linux-musl) + sqlite3 (2.2.0-arm64-darwin) + sqlite3 (2.2.0-x86-linux-gnu) + sqlite3 (2.2.0-x86-linux-musl) + sqlite3 (2.2.0-x86_64-darwin) + sqlite3 (2.2.0-x86_64-linux-gnu) + sqlite3 (2.2.0-x86_64-linux-musl) + stringio (3.1.2) + thor (1.3.2) + timeout (0.4.2) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (2.6.0) + useragent (0.16.10) + websocket-driver (0.7.6) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + zeitwerk (2.7.1) + +PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86-linux + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + bootsnap + brakeman + debug + puma (>= 5.0) + rails (~> 7.2.1) + rubocop-rails-omakase + sqlite3 (>= 1.4) + tzinfo-data + +BUNDLED WITH + 2.5.18 diff --git a/README.md b/README.md index 5b53a0b1..38685320 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,268 @@ +# Steps to install and run the project + +1. Ensure you have Ruby and Rails installed on your system. This project was built using Ruby 3.3.4 and Rails 7.2.2. + +2. Clone the repository. + +3. Navigate to the project directory: + ``` + cd coding-project + ``` + +4. Install the required gems: + ``` + bundle install + ``` + + 4a. Set up credentials: + ```bash + # Generate a new credentials file and master key + rails credentials:edit + ``` + + This will create: + + ``` + - config/credentials.yml.enc + - config/master.key + ``` + + Note: Keep master.key secure and never commit it to the repository + + +5. Create and migrate the database: + ``` + rails db:create + rails db:migrate + rails db:seed # To seed timezone entries + ``` + +6. Start the Rails server: + ``` + rails s + ``` + +The application should now be running at `http://localhost:3000`. + +# API Endpoints + +## Users + +- GET `/users` - List all users +- GET `/users/:id` - Get user details +- POST `/users` - Create a new user +- PUT/PATCH `/users/:id` - Update a user +- DELETE `/users/:id` - Delete a user +- GET `/users/:id/availabilities` - Get user's availabilities +- POST `/users/:id/set_availability` - Set availability for a user +- GET `/users/:id/find_overlap/:other_user_id` - Find overlapping availabilities between two users + +## Timezones + +- GET `/timezones` - List all timezones + +## Availabilities +- GET `/availabilities` - List all availabilities +- GET `/availabilities/:id` - Get availability details +- POST `/availabilities` - Create a new availability +- PUT/PATCH `/availabilities/:id` - Update an availability +- DELETE `/availabilities/:id` - Delete an availability + +## Events + +- GET `/events` - List all events +- GET `/events/:id` - Get event details +- POST `/events` - Create a new event +- PUT/PATCH `/events/:id` - Update an event +- DELETE `/events/:id` - Delete an event +- POST `/events/:id/invite` - Invite a user to an event +- POST `/schedule_meeting/:user_id` - Schedule a meeting with a user + +## Invitations + +- GET `/invitations` - List all invitations +- GET `/invitations/:id` - Get invitation details +- POST `/invitations` - Create a new invitation +- PUT/PATCH `/invitations/:id` - Update an invitation +- DELETE `/invitations/:id` - Delete an invitation + +## Notifications + +- GET `/notifications` - List all notifications +- PUT/PATCH `/notifications/:id` - Update a notification + +# Deploy the project on [Render](https://render.com). + +Follow steps mentioned in [official render rails docs](https://docs.render.com/deploy-rails) + +
+Sample API for POST Requests/Responses + +## Create a User +POST `/users` + +Request: +```json +{ + "user": { + "name": "Rajendra Kadam", + "email": "rajendrakadam249@gmail.com", + "timezone_id": 18 + } +} +``` + +Response: +```json +{ + "id": 1, + "name": "Raj Kadam", + "email": "rajendrakadam249@gmail.com", + "created_at": "2024-11-08T12:07:55.644Z", + "updated_at": "2024-11-08T12:07:55.644Z", + "timezone_id": 18 +} +``` + +## Set Availability +POST `/users/1/set_availability` + +Request: +```json +{ + "availability": { + "start_time": "2024-11-08T09:00:00Z", + "end_time": "2024-11-08T17:00:00Z", + "timezone_id": 18 + } +} +``` + +Response: +```json +{ + "id": 1, + "user_id": 1, + "start_time": "2024-11-08T09:00:00Z", + "end_time": "2024-11-08T17:00:00Z", + "timezone_id": 18, + "created_at": "2024-11-08T12:07:55.644Z", + "updated_at": "2024-11-08T12:07:55.644Z", +} +``` + +## Schedule a Meeting +POST `/schedule_meeting/1` + +Request: +```json +{ + "title": "Project Review", + "description": "Weekly project status review", + "start_time": "2024-11-08T10:00:00Z", + "duration": 60, + "organizer_name": "Raj Kadam", + "organizer_email": "rajkadam@gmail.com" +} +``` + +Response: +```json +{ + "id": 1, + "title": "Project Review", + "description": "Weekly project status review", + "start_time": "2024-11-08T10:00:00Z", + "end_time": "2024-11-08T11:00:00Z", + "duration": 60, + "timezone_id": 18, + "organizer_name": "Raj Kadam", + "organizer_email": "rajkadam@gmail.com", + "created_at": "2024-11-08T12:07:55.644Z", + "updated_at": "2024-11-08T12:07:55.644Z", +} +``` + +## Find Overlapping Availabilities +GET `/users/1/find_overlap/2` + +Response: +```json +[ + { + "start_time": "2024-11-08T10:00:00Z", + "end_time": "2024-11-08T15:00:00Z" + } +] +``` + +## Delete Event +DELETE `/events/1` + +Response: HTTP 204 No Content + +
+ +
+Project Assumptions & Design Decisions + +## Time-Related Assumptions +- All times are stored in UTC for consistency across timezones. +- Duration is stored in minutes for simplicity and human readability. + +## Availability Management +- Users can have multiple non-overlapping availability slots. +- When an event is scheduled, it intelligently splits or consumes the availability slot. +- Adjacent availability slots are automatically adjusted when restored for cleanup. +- Availability can be split into multiple slots when partially consumed, preserving the remaining time slots. + +## Event Handling +- Events have a fixed duration that doesn't change once set. +- Each event requires basic organizer info (name and email) for contact purposes. +- Events can have multiple attendees through invitations. +- Double-booking prevention: events cannot overlap for the same user. +- End time is automatically calculated based on duration and start time. + +## Invitation System +- Simple invitation state machine: pending → accepted/declined. +- Accepting an invitation automatically consumes the user's availability. +- Availability is automatically restored when invitation/event is deleted. +- One user can't have multiple invitations to the same event (prevents duplicates). +- No emails are sent for invitations yet. + +## User Management +- Users require name and unique email for identification. +- Timezone association is mandatory for proper time handling but it is not used anywhere even if supplied in interest of time. +- Users can manage multiple availabilities and invitations. +- All user-related operations maintain data integrity through transactions. + +## Timezone Handling +- Timezones are predefined entities with name and offset. +- All time-based entities (availability, event, user) must specify timezone. +- Timezone names must be unique for consistency. + +## Data Integrity +- All availability modifications are wrapped in transactions. +- Event deletion triggers automatic availability restoration. +- Cascading deletes handle cleanup of dependent relationships. + +## API Design Choices +- JSON responses for modern API compatibility. +- RESTful conventions for predictable endpoints. +- No authentication (MVP approach). +- No rate limiting (can be added later). +- Consistent error responses with appropriate HTTP status codes. + +## Database Design +- Foreign key constraints ensure referential integrity. +- Email uniqueness enforced at database level. +- Dependent relationships use cascading deletes. +- Using SQLite for simplicity (can be upgraded to PostgreSQL/MySQL). + +
+ +--- + # Harbor Take Home Project Welcome to the Harbor take home project. We hope this is a good opportunity for you to showcase your skills. diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..9a5ea738 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +# 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" + +Rails.application.load_tasks diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 00000000..d6726972 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 00000000..0ff5442f --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 00000000..4ac8823b --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,2 @@ +class ApplicationController < ActionController::API +end diff --git a/app/controllers/availabilities_controller.rb b/app/controllers/availabilities_controller.rb new file mode 100644 index 00000000..a8906225 --- /dev/null +++ b/app/controllers/availabilities_controller.rb @@ -0,0 +1,45 @@ +class AvailabilitiesController < ApplicationController + before_action :set_availability, only: [:show, :update, :destroy] + + def index + @availabilities = Availability.all + render json: @availabilities + end + + def show + render json: @availability + end + + def create + @availability = Availability.new(availability_params) + + if @availability.save + render json: @availability, status: :created + else + render json: @availability.errors, status: :unprocessable_entity + end + end + + def update + if @availability.update(availability_params) + render json: @availability, status: :ok + else + render json: @availability.errors, status: :unprocessable_entity + end + end + + def destroy + @availability.destroy + head :no_content + end + + private + + def set_availability + @availability = Availability.find(params[:id]) + end + + def availability_params + params.require(:availability).permit(:start_time, :end_time, :user_id, :timezone_id) + end +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 00000000..e69de29b diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb new file mode 100644 index 00000000..bbee64a1 --- /dev/null +++ b/app/controllers/events_controller.rb @@ -0,0 +1,88 @@ +class EventsController < ApplicationController + before_action :set_event, only: [:show, :update, :destroy, :invite] + + def index + @events = Event.all + render json: @events + end + + def show + render json: @event + end + + def create + @event = Event.new(event_params) + + if @event.save + render json: @event, status: :created + else + render json: @event.errors, status: :unprocessable_entity + end + end + + def update + if @event.update(event_params) + render json: @event, status: :ok + else + render json: @event.errors, status: :unprocessable_entity + end + end + + def destroy + @event.destroy + head :no_content + end + + def invite + @user = User.find(params[:user_id]) + @invitation = @event.invitations.build(user: @user, status: :pending) + + if @invitation.save + render json: @invitation, status: :created + else + render json: @invitation.errors, status: :unprocessable_entity + end + end + + def schedule_meeting + @user = User.find(params[:user_id]) + start_time = params[:start_time].to_datetime + end_time = start_time + params[:duration].minutes + @availability = Availability.find_available_slot(@user, start_time, end_time) + + if @availability + @event = Event.new( + title: params[:title], + description: params[:description], + start_time: start_time, + duration: params[:duration], + timezone: @availability.timezone, + organizer_name: params[:organizer_name], + organizer_email: params[:organizer_email] + ) + + if @event.save + @invitation = @event.invitations.build(user: @user, status: :accepted) + @invitation.save! + + @availability.consume_duration(@event.start_time, @event.end_time) + + render json: @event, status: :created + else + render json: @event.errors, status: :unprocessable_entity + end + else + render json: { error: "No available slot found for the requested time and duration" }, status: :unprocessable_entity + end + end + + private + + def set_event + @event = Event.find(params[:id]) + end + + def event_params + params.require(:event).permit(:title, :description, :start_time, :duration, :timezone_id, :organizer_name, :organizer_email) + end +end diff --git a/app/controllers/invitations_controller.rb b/app/controllers/invitations_controller.rb new file mode 100644 index 00000000..c448f94b --- /dev/null +++ b/app/controllers/invitations_controller.rb @@ -0,0 +1,66 @@ +class InvitationsController < ApplicationController + before_action :set_invitation, only: [ :show, :update, :destroy ] + before_action :set_event, only: [ :create ] + before_action :set_user, only: [ :create ] + + def index + @invitations = Invitation.all + render json: @invitations + end + + def show + render json: @invitation + end + + def create + @availability = Availability.find_available_slot(@user, @event.start_time, @event.end_time) + + if @availability + ActiveRecord::Base.transaction do + # Create invitation + @invitation = Invitation.new(invitation_params) + + if @invitation.save + # Consume the time slot from availability + @availability.consume_duration(@event.start_time, @event.end_time) + render json: @invitation, status: :created + else + render json: @invitation.errors, status: :unprocessable_entity + end + end + else + render json: { error: "User is not available for this time slot" }, status: :unprocessable_entity + end + end + + def update + if @invitation.update(invitation_params) + render json: @invitation, status: :ok + else + render json: @invitation.errors, status: :unprocessable_entity + end + end + + def destroy + @invitation.destroy + head :no_content + end + + private + + def set_invitation + @invitation = Invitation.find(params[:id]) + end + + def set_event + @event = Event.find(invitation_params[:event_id]) + end + + def set_user + @user = User.find(invitation_params[:user_id]) + end + + def invitation_params + params.require(:invitation).permit(:event_id, :user_id, :status) + end +end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb new file mode 100644 index 00000000..fe655625 --- /dev/null +++ b/app/controllers/notifications_controller.rb @@ -0,0 +1,45 @@ +class NotificationsController < ApplicationController + before_action :set_notification, only: [:show, :update, :destroy] + + def index + @notifications = current_user.notifications + render json: @notifications + end + + def show + render json: @notification + end + + def create + @notification = Notification.new(notification_params) + + if @notification.save + render json: @notification, status: :created + else + render json: @notification.errors, status: :unprocessable_entity + end + end + + def update + if @notification.update(notification_params) + render json: @notification, status: :ok + else + render json: @notification.errors, status: :unprocessable_entity + end + end + + def destroy + @notification.destroy + head :no_content + end + + private + + def set_notification + @notification = Notification.find(params[:id]) + end + + def notification_params + params.require(:notification).permit(:user_id, :notifiable_id, :notifiable_type, :notification_type, :read_at) + end +end diff --git a/app/controllers/timezones_controller.rb b/app/controllers/timezones_controller.rb new file mode 100644 index 00000000..312bca93 --- /dev/null +++ b/app/controllers/timezones_controller.rb @@ -0,0 +1,45 @@ +class TimezonesController < ApplicationController + before_action :set_timezone, only: [:show, :update, :destroy] + + def index + @timezones = Timezone.all + render json: @timezones + end + + def show + render json: @timezone + end + + def create + @timezone = Timezone.new(timezone_params) + + if @timezone.save + render json: @timezone, status: :created + else + render json: @timezone.errors, status: :unprocessable_entity + end + end + + def update + if @timezone.update(timezone_params) + render json: @timezone, status: :ok + else + render json: @timezone.errors, status: :unprocessable_entity + end + end + + def destroy + @timezone.destroy + head :no_content + end + + private + + def set_timezone + @timezone = Timezone.find(params[:id]) + end + + def timezone_params + params.require(:timezone).permit(:name, :offset) + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 00000000..81254560 --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,78 @@ +class UsersController < ApplicationController + before_action :set_user, only: [ :availabilities, :set_availability, :find_overlap ] + + def index + @users = User.all + + render json: @users + end + + def show + render json: @user + end + + def create + @user = User.new(user_params) + + if @user.save + render json: @user, status: :created + else + render json: @user.errors, status: :unprocessable_entity + end + end + + def update + if @user.update(user_params) + render json: @user, status: :ok + else + render json: @user.errors, status: :unprocessable_entity + end + end + + def destroy + @user.destroy + head :no_content + end + + def availabilities + @availabilities = @user.availabilities + render json: @availabilities + end + + def set_availability + @availability = @user.availabilities.build(availability_params) + + if @availability.save + render json: @availability, status: :created + else + render json: @availability.errors, status: :unprocessable_entity + end + end + + def find_overlap + @other_user = User.find_by(id: params[:other_user_id]) + + if @user.nil? || @other_user.nil? + render json: { error: "User not found" }, status: :not_found + return + end + + @overlapping_availabilities = Availability.find_overlapping_availabilities(@user.id, @other_user.id) + render json: @overlapping_availabilities + end + + private + + def set_user + user_id = params[:id] || params[:user_id] + @user = User.find_by(id: user_id) + end + + def user_params + params.require(:user).permit(:name, :email, :timezone_id) + end + + def availability_params + params.require(:availability).permit(:start_time, :end_time, :timezone_id) + end +end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 00000000..d394c3d1 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 00000000..3c34c814 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout "mailer" +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 00000000..b63caeb8 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/app/models/availability.rb b/app/models/availability.rb new file mode 100644 index 00000000..9234557a --- /dev/null +++ b/app/models/availability.rb @@ -0,0 +1,93 @@ +class Availability < ApplicationRecord + belongs_to :user + belongs_to :timezone + + validates :start_time, presence: true + validates :end_time, presence: true + validate :end_time_after_start_time + + def self.find_overlapping_availabilities(user1_id, user2_id) + Availability + .where(user_id: user1_id) + .joins("INNER JOIN availabilities AS other_avail ON other_avail.user_id = #{user2_id}") + .where("availabilities.start_time < other_avail.end_time AND availabilities.end_time > other_avail.start_time") + .select( + "CASE WHEN availabilities.start_time > other_avail.start_time THEN availabilities.start_time ELSE other_avail.start_time END AS start_time", + "CASE WHEN availabilities.end_time < other_avail.end_time THEN availabilities.end_time ELSE other_avail.end_time END AS end_time" + ) + .map { |avail| { start_time: avail.start_time, end_time: avail.end_time } } + end + + def self.find_available_slot(user, start_time, end_time) + # First find a potential availability slot + availability = user.availabilities + .where("start_time <= ? AND end_time >= ?", end_time, start_time) + .first + + return nil unless availability + + # Check if there are any existing events that would overlap + has_overlapping_events = Event.joins(:invitations) + .where(invitations: { user_id: user.id }) + .where("start_time < ? AND end_time > ?", end_time, start_time) + .exists? + + has_overlapping_events ? nil : availability + end + + def consume_duration(start_time, end_time) + if self.start_time < start_time && self.end_time > end_time + # case of booking in middle of the availability + remaining_start = self.dup + remaining_start.end_time = start_time + remaining_start.save! + + remaining_end = self.dup + remaining_end.start_time = end_time + remaining_end.save! + + self.destroy + elsif self.start_time >= start_time && self.end_time <= end_time + # The entire availability is consumed, no need to split + self.destroy + else + # Partial overlap, adjust the start_time or end_time accordingly + if self.start_time < start_time + self.update!(end_time: start_time) + else + self.update!(start_time: end_time) + end + end + end + + def self.restore_slot(user, start_time, end_time) + return if user.availabilities.exists?(start_time: start_time, end_time: end_time) + # Find adjacent or overlapping availabilities + adjacent_slots = user.availabilities + .where("(end_time = ? OR start_time = ?)", start_time, end_time) + .order(:start_time) + + if adjacent_slots.empty? + # Create new availability if no adjacent slots + user.availabilities.create!( + start_time: start_time, + end_time: end_time, + timezone: user.timezone + ) + else + adjacent_slots.each do |slot| + if slot.end_time == start_time + # Extend the existing slot's end time + slot.update!(end_time: end_time) + elsif slot.start_time == end_time + # Extend the existing slot's start time + slot.update!(start_time: start_time) + end + end + end + end + + def end_time_after_start_time + errors.add(:end_time, "must be after start time") if end_time <= start_time + end +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 00000000..e69de29b diff --git a/app/models/event.rb b/app/models/event.rb new file mode 100644 index 00000000..4236b6d9 --- /dev/null +++ b/app/models/event.rb @@ -0,0 +1,33 @@ +class Event < ApplicationRecord + belongs_to :timezone + has_many :invitations, dependent: :destroy + has_many :attendees, through: :invitations, source: :user + + validates :title, presence: true + validates :start_time, presence: true + validates :duration, presence: true, numericality: { greater_than: 0 } + validate :end_time_after_start_time + validates :organizer_name, presence: true + validates :organizer_email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP } + + before_validation :set_end_time + before_destroy :restore_availability_slots + + private + + def end_time_after_start_time + errors.add(:end_time, "must be after start time") if end_time <= start_time + end + + def set_end_time + self.end_time = start_time + duration.minutes if start_time.present? && duration.present? + end + + private + + def restore_availability_slots + invitations.each do |invitation| + Availability.restore_slot(invitation.user, start_time, end_time) + end + end +end diff --git a/app/models/invitation.rb b/app/models/invitation.rb new file mode 100644 index 00000000..7aea7565 --- /dev/null +++ b/app/models/invitation.rb @@ -0,0 +1,18 @@ +class Invitation < ApplicationRecord + belongs_to :event + belongs_to :user + + enum status: { pending: 0, accepted: 1, declined: 2 } + + validates :event, presence: true + validates :user, presence: true + validates :status, presence: true + + before_destroy :restore_availability_slot + + private + + def restore_availability_slot + Availability.restore_slot(user, event.start_time, event.end_time) + end +end diff --git a/app/models/notification.rb b/app/models/notification.rb new file mode 100644 index 00000000..87176552 --- /dev/null +++ b/app/models/notification.rb @@ -0,0 +1,10 @@ +class Notification < ApplicationRecord + belongs_to :user + belongs_to :notifiable + + enum notification_type: { invitation: 0, reminder: 1 } + + validates :user, presence: true + validates :notifiable, presence: true + validates :notification_type, presence: true +end diff --git a/app/models/timezone.rb b/app/models/timezone.rb new file mode 100644 index 00000000..e1b97569 --- /dev/null +++ b/app/models/timezone.rb @@ -0,0 +1,8 @@ +class Timezone < ApplicationRecord + has_many :users + has_many :availabilities + has_many :events + + validates :name, presence: true, uniqueness: true + validates :offset, presence: true +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 00000000..2051bb1d --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,10 @@ +class User < ApplicationRecord + belongs_to :timezone, class_name: "Timezone" + + has_many :availabilities, dependent: :destroy + has_many :invitations, dependent: :destroy + has_many :notifications, dependent: :destroy + + validates :name, presence: true + validates :email, presence: true, uniqueness: true +end diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 00000000..3aac9002 --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 00000000..37f0bddb --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/bin/brakeman b/bin/brakeman new file mode 100755 index 00000000..ace1c9ba --- /dev/null +++ b/bin/brakeman @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +ARGV.unshift("--ensure-latest") + +load Gem.bin_path("brakeman", "brakeman") diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 00000000..50da5fdf --- /dev/null +++ b/bin/bundle @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN) + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../Gemfile", __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || + cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + bundler_gem_version.approximate_recommendation + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint new file mode 100755 index 00000000..840d093a --- /dev/null +++ b/bin/docker-entrypoint @@ -0,0 +1,13 @@ +#!/bin/bash -e + +# Enable jemalloc for reduced memory usage and latency. +if [ -z "${LD_PRELOAD+x}" ] && [ -f /usr/lib/*/libjemalloc.so.2 ]; then + export LD_PRELOAD="$(echo /usr/lib/*/libjemalloc.so.2)" +fi + +# If running the rails server then create or migrate existing database +if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then + ./bin/rails db:prepare +fi + +exec "${@}" diff --git a/bin/rails b/bin/rails new file mode 100755 index 00000000..efc03774 --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake new file mode 100755 index 00000000..4fbf10b9 --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/bin/render-build.sh b/bin/render-build.sh new file mode 100755 index 00000000..a76116ef --- /dev/null +++ b/bin/render-build.sh @@ -0,0 +1,5 @@ +set -o errexit + +bundle install +bundle exec rails db:migrate +bundle exec rails db:seed \ No newline at end of file diff --git a/bin/rubocop b/bin/rubocop new file mode 100755 index 00000000..40330c0f --- /dev/null +++ b/bin/rubocop @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +# explicit rubocop config increases performance slightly while avoiding config confusion. +ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) + +load Gem.bin_path("rubocop", "rubocop") diff --git a/bin/setup b/bin/setup new file mode 100755 index 00000000..ce57f3fc --- /dev/null +++ b/bin/setup @@ -0,0 +1,37 @@ +#!/usr/bin/env ruby +require "fileutils" + +APP_ROOT = File.expand_path("..", __dir__) +APP_NAME = "coding-project" + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system! "gem install bundler --conservative" + system("bundle check") || system!("bundle install") + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + puts "\n== Restarting application server ==" + system! "bin/rails restart" + + # puts "\n== Configuring puma-dev ==" + # system "ln -nfs #{APP_ROOT} ~/.puma-dev/#{APP_NAME}" + # system "curl -Is https://#{APP_NAME}.test/up | head -n 1" +end diff --git a/config.ru b/config.ru new file mode 100644 index 00000000..4a3c09a6 --- /dev/null +++ b/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 00000000..53757b69 --- /dev/null +++ b/config/application.rb @@ -0,0 +1,32 @@ +require_relative "boot" + +require "rails/all" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module CodingProject + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 7.2 + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + + # Only loads a smaller set of middleware suitable for API only apps. + # Middleware like session, flash, cookies can be added back manually. + # Skip views, helpers and assets when generating a new resource. + config.api_only = true + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 00000000..988a5ddc --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,4 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 00000000..b363a56c --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: coding_project_production diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc new file mode 100644 index 00000000..a0125357 --- /dev/null +++ b/config/credentials.yml.enc @@ -0,0 +1 @@ +H3LezCSof6O2bBmSMMdvI1IlO1O5MHfjvTqL9knzSOG2QBVpxfo5iFXYdvsMWMpB59KgetZFNmByAMgQEVmQ8I8FxODNpnIwHWMVVt0K9V+Ddo8cwVJWgegR8i52yEDZAK7uFI9hXWAIspiztopQAGoi13ysCtNZPi3QU370uw279w/rPcxmGDb0uQuTP1N8dAAJ2Ryyp/fne0rT3wg7eQhSrEwSR/DGWPvtiVoDbxAWL0Psl9zajCbR9zpunRgg0AiyvDSMQj236uoVwu1bcCLxl+J7QnKDB6ZYsc77SQMZceolmBQKTbWWt+ymipyxKSClvi8IvaUmwB0fQjqRlBHYXnV2uR5oP0gOSrJC0HpFYVxO6kMSIc+aJaax3urpjOpXKzBTBq4QSpqOi7YSYP4+CEHU--sap8vvNohTOwL1P5--SkG16JHSIHHgMo6EYI6iDw== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 00000000..6d5cae3a --- /dev/null +++ b/config/database.yml @@ -0,0 +1,32 @@ +# SQLite. Versions 3.8.0 and up are supported. +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem "sqlite3" +# +default: &default + adapter: sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + <<: *default + database: storage/development.sqlite3 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: storage/test.sqlite3 + + +# SQLite3 write its data on the local filesystem, as such it requires +# persistent disks. If you are deploying to a managed service, you should +# make sure it provides disk persistence, as many don't. +# +# Similarly, if you deploy your application as a Docker container, you must +# ensure the database is located in a persisted volume. +production: + <<: *default + database: storage/production.sqlite3 diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 00000000..cac53157 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 00000000..98128ff2 --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,75 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing. + config.server_timing = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.cache_store = :memory_store + config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + # Disable caching for Action Mailer templates even if Action Controller + # caching is enabled. + config.action_mailer.perform_caching = false + + config.action_mailer.default_url_options = { host: "localhost", port: 3000 } + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true + + # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. + # config.generators.apply_rubocop_autocorrect_after_generate! +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 00000000..3961a195 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,95 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + + # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment + # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. + # config.public_file_server.enabled = false + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Mount Action Cable outside main process or domain. + # config.action_cable.mount_path = nil + # config.action_cable.url = "wss://example.com/cable" + # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. + # config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = true + + # Skip http-to-https redirect for the default health check endpoint. + # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } + + # Log to STDOUT by default + config.logger = ActiveSupport::Logger.new(STDOUT) + .tap { |logger| logger.formatter = ::Logger::Formatter.new } + .then { |logger| ActiveSupport::TaggedLogging.new(logger) } + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # "info" includes generic and useful information about system operation, but avoids logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). If you + # want to log everything, set the level to "debug". + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "coding_project_production" + + # Disable caching for Action Mailer templates even if Action Controller + # caching is enabled. + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 00000000..0c616a1b --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,67 @@ +require "active_support/core_ext/integer/time" + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{1.hour.to_i}" } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.cache_store = :null_store + + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + # Disable caching for Action Mailer templates even if Action Controller + # caching is enabled. + config.action_mailer.perform_caching = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Unlike controllers, the mailer instance doesn't have any context about the + # incoming request so you'll need to provide the :host parameter yourself. + config.action_mailer.default_url_options = { host: "www.example.com" } + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb new file mode 100644 index 00000000..0c5dd99a --- /dev/null +++ b/config/initializers/cors.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Avoid CORS issues when API is called from the frontend app. +# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin Ajax requests. + +# Read more: https://github.com/cyu/rack-cors + +# Rails.application.config.middleware.insert_before 0, Rack::Cors do +# allow do +# origins "example.com" +# +# resource "*", +# headers: :any, +# methods: [:get, :post, :put, :patch, :delete, :options, :head] +# end +# end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 00000000..c010b83d --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 00000000..3860f659 --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym "RESTful" +# end diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 00000000..6c349ae5 --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,31 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + hello: "Hello world" diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 00000000..03c166f4 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,34 @@ +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. + +# Puma starts a configurable number of processes (workers) and each process +# serves each request in a thread from an internal thread pool. +# +# The ideal number of threads per worker depends both on how much time the +# application spends waiting for IO operations and on how much you wish to +# to prioritize throughput over latency. +# +# As a rule of thumb, increasing the number of threads will increase how much +# traffic a given process can handle (throughput), but due to CRuby's +# Global VM Lock (GVL) it has diminishing returns and will degrade the +# response time (latency) of the application. +# +# The default is set to 3 threads as it's deemed a decent compromise between +# throughput and latency for the average Rails application. +# +# Any libraries that use a connection pool or another resource pool should +# be configured to provide at least as many connections as the number of +# threads. This includes Active Record's `pool` parameter in `database.yml`. +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT", 3000) + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart + +# Specify the PID file. Defaults to tmp/pids/server.pid in development. +# In other environments, only set the PID file if requested. +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 00000000..6fbcb55c --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,25 @@ +Rails.application.routes.draw do + resources :users do + member do + get "availabilities" + post "set_availability" + end + get "find_overlap/:other_user_id", to: "users#find_overlap", as: "find_overlap" + end + + resources :timezones, only: [ :index ] + + resources :availabilities, except: [ :new, :edit ] + + post "schedule_meeting/:user_id", to: "events#schedule_meeting", as: "schedule_meeting" + + resources :events do + member do + post "invite" + end + end + + resources :invitations + + resources :notifications, only: [ :index, :update ] +end diff --git a/config/storage.yml b/config/storage.yml new file mode 100644 index 00000000..4942ab66 --- /dev/null +++ b/config/storage.yml @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/db/migrate/20241107101000_create_users.rb b/db/migrate/20241107101000_create_users.rb new file mode 100644 index 00000000..9d3efebb --- /dev/null +++ b/db/migrate/20241107101000_create_users.rb @@ -0,0 +1,12 @@ +class CreateUsers < ActiveRecord::Migration[7.2] + def change + create_table :users do |t| + t.string :name + t.string :email + t.references :time_zone, null: false, foreign_key: true + + t.timestamps + end + add_index :users, :email, unique: true + end +end diff --git a/db/migrate/20241107101003_create_timezones.rb b/db/migrate/20241107101003_create_timezones.rb new file mode 100644 index 00000000..33acb466 --- /dev/null +++ b/db/migrate/20241107101003_create_timezones.rb @@ -0,0 +1,11 @@ +class CreateTimezones < ActiveRecord::Migration[7.2] + def change + create_table :timezones do |t| + t.string :name + t.integer :offset + + t.timestamps + end + add_index :timezones, :name, unique: true + end +end diff --git a/db/migrate/20241107101009_create_availabilities.rb b/db/migrate/20241107101009_create_availabilities.rb new file mode 100644 index 00000000..861aa988 --- /dev/null +++ b/db/migrate/20241107101009_create_availabilities.rb @@ -0,0 +1,14 @@ +class CreateAvailabilities < ActiveRecord::Migration[7.2] + def change + create_table :availabilities do |t| + t.datetime :start_time + t.datetime :end_time + t.text :recurring_pattern + t.references :user, null: false, foreign_key: true + t.references :timezone, null: false, foreign_key: true + + t.timestamps + end + add_index :availabilities, [:user_id, :start_time, :end_time], unique: true, name: 'index_availabilities_on_user_id_and_times' + end +end diff --git a/db/migrate/20241107101013_create_events.rb b/db/migrate/20241107101013_create_events.rb new file mode 100644 index 00000000..c80f854c --- /dev/null +++ b/db/migrate/20241107101013_create_events.rb @@ -0,0 +1,13 @@ +class CreateEvents < ActiveRecord::Migration[7.2] + def change + create_table :events do |t| + t.string :title + t.text :description + t.datetime :start_time + t.datetime :end_time + t.references :timezone, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20241107101016_create_invitations.rb b/db/migrate/20241107101016_create_invitations.rb new file mode 100644 index 00000000..044ada5b --- /dev/null +++ b/db/migrate/20241107101016_create_invitations.rb @@ -0,0 +1,11 @@ +class CreateInvitations < ActiveRecord::Migration[7.2] + def change + create_table :invitations do |t| + t.references :event, null: false, foreign_key: true + t.references :user, null: false, foreign_key: true + t.integer :status + + t.timestamps + end + end +end diff --git a/db/migrate/20241107101020_create_notifications.rb b/db/migrate/20241107101020_create_notifications.rb new file mode 100644 index 00000000..3ce8255c --- /dev/null +++ b/db/migrate/20241107101020_create_notifications.rb @@ -0,0 +1,13 @@ +class CreateNotifications < ActiveRecord::Migration[7.2] + def change + create_table :notifications do |t| + t.references :user, null: false, foreign_key: true + t.references :notifiable, null: false, foreign_key: true + t.string :notifiable_type + t.string :notification_type + t.datetime :read_at + + t.timestamps + end + end +end diff --git a/db/migrate/20241107104001_add_organizer_info_to_events.rb b/db/migrate/20241107104001_add_organizer_info_to_events.rb new file mode 100644 index 00000000..34b82b72 --- /dev/null +++ b/db/migrate/20241107104001_add_organizer_info_to_events.rb @@ -0,0 +1,6 @@ +class AddOrganizerInfoToEvents < ActiveRecord::Migration[7.2] + def change + add_column :events, :organizer_name, :string + add_column :events, :organizer_email, :string + end +end diff --git a/db/migrate/20241107114419_add_duration_to_events.rb b/db/migrate/20241107114419_add_duration_to_events.rb new file mode 100644 index 00000000..24bf7241 --- /dev/null +++ b/db/migrate/20241107114419_add_duration_to_events.rb @@ -0,0 +1,5 @@ +class AddDurationToEvents < ActiveRecord::Migration[7.2] + def change + add_column :events, :duration, :integer + end +end diff --git a/db/migrate/20241107121400_change_offset_type_in_timezones.rb b/db/migrate/20241107121400_change_offset_type_in_timezones.rb new file mode 100644 index 00000000..06366714 --- /dev/null +++ b/db/migrate/20241107121400_change_offset_type_in_timezones.rb @@ -0,0 +1,5 @@ +class ChangeOffsetTypeInTimezones < ActiveRecord::Migration[7.2] + def change + change_column :timezones, :offset, :string + end +end diff --git a/db/migrate/20241107122034_rename_time_zone_id_to_timezone_id_in_users.rb b/db/migrate/20241107122034_rename_time_zone_id_to_timezone_id_in_users.rb new file mode 100644 index 00000000..5e86778b --- /dev/null +++ b/db/migrate/20241107122034_rename_time_zone_id_to_timezone_id_in_users.rb @@ -0,0 +1,6 @@ +class RenameTimeZoneIdToTimezoneIdInUsers < ActiveRecord::Migration[7.2] + def change + remove_column :users, :time_zone_id + add_column :users, :timezone_id, :integer, null: false + end +end diff --git a/db/migrate/20241107164325_drop_recurring_pattern_col_on_availability.rb b/db/migrate/20241107164325_drop_recurring_pattern_col_on_availability.rb new file mode 100644 index 00000000..78d9cfad --- /dev/null +++ b/db/migrate/20241107164325_drop_recurring_pattern_col_on_availability.rb @@ -0,0 +1,5 @@ +class DropRecurringPatternColOnAvailability < ActiveRecord::Migration[7.2] + def change + remove_column :availabilities, :recurring_pattern + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 00000000..e8e1989c --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,86 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[7.2].define(version: 2024_11_07_164325) do + create_table "availabilities", force: :cascade do |t| + t.datetime "start_time" + t.datetime "end_time" + t.integer "user_id", null: false + t.integer "timezone_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["timezone_id"], name: "index_availabilities_on_timezone_id" + t.index ["user_id", "start_time", "end_time"], name: "index_availabilities_on_user_id_and_times", unique: true + t.index ["user_id"], name: "index_availabilities_on_user_id" + end + + create_table "events", force: :cascade do |t| + t.string "title" + t.text "description" + t.datetime "start_time" + t.datetime "end_time" + t.integer "timezone_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "organizer_name" + t.string "organizer_email" + t.integer "duration" + t.index ["timezone_id"], name: "index_events_on_timezone_id" + end + + create_table "invitations", force: :cascade do |t| + t.integer "event_id", null: false + t.integer "user_id", null: false + t.integer "status" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["event_id"], name: "index_invitations_on_event_id" + t.index ["user_id"], name: "index_invitations_on_user_id" + end + + create_table "notifications", force: :cascade do |t| + t.integer "user_id", null: false + t.integer "notifiable_id", null: false + t.string "notifiable_type" + t.string "notification_type" + t.datetime "read_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["notifiable_id"], name: "index_notifications_on_notifiable_id" + t.index ["user_id"], name: "index_notifications_on_user_id" + end + + create_table "timezones", force: :cascade do |t| + t.string "name" + t.string "offset" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["name"], name: "index_timezones_on_name", unique: true + end + + create_table "users", force: :cascade do |t| + t.string "name" + t.string "email" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "timezone_id", null: false + t.index ["email"], name: "index_users_on_email", unique: true + end + + add_foreign_key "availabilities", "timezones" + add_foreign_key "availabilities", "users" + add_foreign_key "events", "timezones" + add_foreign_key "invitations", "events" + add_foreign_key "invitations", "users" + add_foreign_key "notifications", "notifiables" + add_foreign_key "notifications", "users" +end diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 00000000..e2f6e817 --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,30 @@ +# Timezones +Timezone.create([ + { name: 'Pacific/Midway', offset: '-11:00' }, + { name: 'Pacific/Honolulu', offset: '-10:00' }, + { name: 'America/Anchorage', offset: '-9:00' }, + { name: 'America/Los_Angeles', offset: '-8:00' }, + { name: 'America/Denver', offset: '-7:00' }, + { name: 'America/Chicago', offset: '-6:00' }, + { name: 'America/New_York', offset: '-5:00' }, + { name: 'America/Halifax', offset: '-4:00' }, + { name: 'America/Godthab', offset: '-3:00' }, + { name: 'Atlantic/Azores', offset: '-1:00' }, + { name: 'Europe/London', offset: '0:00' }, + { name: 'Europe/Berlin', offset: '+1:00' }, + { name: 'Europe/Athens', offset: '+2:00' }, + { name: 'Europe/Moscow', offset: '+3:00' }, + { name: 'Asia/Dubai', offset: '+4:00' }, + { name: 'Asia/Kabul', offset: '+4:30' }, + { name: 'Asia/Karachi', offset: '+5:00' }, + { name: 'Asia/Kolkata', offset: '+5:30' }, + { name: 'Asia/Kathmandu', offset: '+5:45' }, + { name: 'Asia/Dhaka', offset: '+6:00' }, + { name: 'Asia/Rangoon', offset: '+6:30' }, + { name: 'Asia/Bangkok', offset: '+7:00' }, + { name: 'Asia/Singapore', offset: '+8:00' }, + { name: 'Asia/Tokyo', offset: '+9:00' }, + { name: 'Australia/Sydney', offset: '+10:00' }, + { name: 'Pacific/Noumea', offset: '+11:00' }, + { name: 'Pacific/Auckland', offset: '+12:00' } +]) diff --git a/lib/tasks/.keep b/lib/tasks/.keep new file mode 100644 index 00000000..e69de29b diff --git a/log/.keep b/log/.keep new file mode 100644 index 00000000..e69de29b diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..c19f78ab --- /dev/null +++ b/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/render.yaml b/render.yaml new file mode 100644 index 00000000..4c99db24 --- /dev/null +++ b/render.yaml @@ -0,0 +1,7 @@ +services: + - type: web + name: mysite + runtime: ruby + plan: free + buildCommand: "./bin/render-build.sh" + startCommand: "bundle exec rails server" \ No newline at end of file diff --git a/storage/.keep b/storage/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/channels/application_cable/connection_test.rb b/test/channels/application_cable/connection_test.rb new file mode 100644 index 00000000..6340bf9c --- /dev/null +++ b/test/channels/application_cable/connection_test.rb @@ -0,0 +1,13 @@ +require "test_helper" + +module ApplicationCable + class ConnectionTest < ActionCable::Connection::TestCase + # test "connects with cookies" do + # cookies.signed[:user_id] = 42 + # + # connect + # + # assert_equal connection.user_id, "42" + # end + end +end diff --git a/test/controllers/.keep b/test/controllers/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/controllers/availabilities_controller_test.rb b/test/controllers/availabilities_controller_test.rb new file mode 100644 index 00000000..ce8fe77d --- /dev/null +++ b/test/controllers/availabilities_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class AvailabilitiesControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/events_controller_test.rb b/test/controllers/events_controller_test.rb new file mode 100644 index 00000000..d2243abe --- /dev/null +++ b/test/controllers/events_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class EventsControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/invitations_controller_test.rb b/test/controllers/invitations_controller_test.rb new file mode 100644 index 00000000..e2c1b428 --- /dev/null +++ b/test/controllers/invitations_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class InvitationsControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/notifications_controller_test.rb b/test/controllers/notifications_controller_test.rb new file mode 100644 index 00000000..20b28201 --- /dev/null +++ b/test/controllers/notifications_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class NotificationsControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/timezones_controller_test.rb b/test/controllers/timezones_controller_test.rb new file mode 100644 index 00000000..f592e658 --- /dev/null +++ b/test/controllers/timezones_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class TimezonesControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb new file mode 100644 index 00000000..61c15322 --- /dev/null +++ b/test/controllers/users_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class UsersControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/fixtures/availabilities.yml b/test/fixtures/availabilities.yml new file mode 100644 index 00000000..82feea8f --- /dev/null +++ b/test/fixtures/availabilities.yml @@ -0,0 +1,13 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + start_time: 2024-11-07 15:40:09 + end_time: 2024-11-07 15:40:09 + user: one + timezone: one + +two: + start_time: 2024-11-07 15:40:09 + end_time: 2024-11-07 15:40:09 + user: two + timezone: two diff --git a/test/fixtures/events.yml b/test/fixtures/events.yml new file mode 100644 index 00000000..2e6ef90c --- /dev/null +++ b/test/fixtures/events.yml @@ -0,0 +1,15 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + title: MyString + description: MyText + start_time: 2024-11-07 15:40:13 + end_time: 2024-11-07 15:40:13 + timezone: one + +two: + title: MyString + description: MyText + start_time: 2024-11-07 15:40:13 + end_time: 2024-11-07 15:40:13 + timezone: two diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/invitations.yml b/test/fixtures/invitations.yml new file mode 100644 index 00000000..0acbe8ee --- /dev/null +++ b/test/fixtures/invitations.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + event: one + user: one + status: 1 + +two: + event: two + user: two + status: 1 diff --git a/test/fixtures/notifications.yml b/test/fixtures/notifications.yml new file mode 100644 index 00000000..92a891f0 --- /dev/null +++ b/test/fixtures/notifications.yml @@ -0,0 +1,15 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + user: one + notifiable: one + notifiable_type: MyString + notification_type: MyString + read_at: 2024-11-07 15:40:20 + +two: + user: two + notifiable: two + notifiable_type: MyString + notification_type: MyString + read_at: 2024-11-07 15:40:20 diff --git a/test/fixtures/timezones.yml b/test/fixtures/timezones.yml new file mode 100644 index 00000000..b56e1b15 --- /dev/null +++ b/test/fixtures/timezones.yml @@ -0,0 +1,9 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + name: MyString + offset: 1 + +two: + name: MyString + offset: 1 diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml new file mode 100644 index 00000000..ad314656 --- /dev/null +++ b/test/fixtures/users.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + name: MyString + email: MyString + time_zone: one + +two: + name: MyString + email: MyString + time_zone: two diff --git a/test/integration/.keep b/test/integration/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/mailers/.keep b/test/mailers/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/models/.keep b/test/models/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/models/availability_test.rb b/test/models/availability_test.rb new file mode 100644 index 00000000..f5095373 --- /dev/null +++ b/test/models/availability_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class AvailabilityTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/event_test.rb b/test/models/event_test.rb new file mode 100644 index 00000000..c8465c19 --- /dev/null +++ b/test/models/event_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class EventTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/invitation_test.rb b/test/models/invitation_test.rb new file mode 100644 index 00000000..a7debdea --- /dev/null +++ b/test/models/invitation_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class InvitationTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/notification_test.rb b/test/models/notification_test.rb new file mode 100644 index 00000000..a76e08d6 --- /dev/null +++ b/test/models/notification_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class NotificationTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/timezone_test.rb b/test/models/timezone_test.rb new file mode 100644 index 00000000..bf1c97d0 --- /dev/null +++ b/test/models/timezone_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class TimezoneTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/user_test.rb b/test/models/user_test.rb new file mode 100644 index 00000000..5c07f490 --- /dev/null +++ b/test/models/user_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class UserTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 00000000..0c22470e --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,15 @@ +ENV["RAILS_ENV"] ||= "test" +require_relative "../config/environment" +require "rails/test_help" + +module ActiveSupport + class TestCase + # Run tests in parallel with specified workers + parallelize(workers: :number_of_processors) + + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all + + # Add more helper methods to be used by all tests here... + end +end diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 00000000..e69de29b diff --git a/tmp/pids/.keep b/tmp/pids/.keep new file mode 100644 index 00000000..e69de29b diff --git a/tmp/storage/.keep b/tmp/storage/.keep new file mode 100644 index 00000000..e69de29b diff --git a/vendor/.keep b/vendor/.keep new file mode 100644 index 00000000..e69de29b