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