diff --git a/.gitignore b/.gitignore index e16dc71d2..169586125 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # Ignore bundler config. /.bundle +/coverage # Ignore all logfiles and tempfiles. /log/* diff --git a/.rspec b/.rspec new file mode 100644 index 000000000..c99d2e739 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/Gemfile b/Gemfile index a8a68a722..86cf2d390 100644 --- a/Gemfile +++ b/Gemfile @@ -1,31 +1,33 @@ -source "https://rubygems.org" +# frozen_string_literal: true + +source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby "3.2.2" +ruby '3.2.2' # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" -gem "rails", "~> 7.0.4", ">= 7.0.4.2" +gem 'rails', '~> 7.0.4', '>= 7.0.4.2' # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] -gem "sprockets-rails" +gem 'sprockets-rails' # Use postgresql as the database for Active Record -gem "pg", "~> 1.1" +gem 'pg', '~> 1.1' # Use the Puma web server [https://github.com/puma/puma] -gem "puma", "~> 5.0" +gem 'puma', '~> 5.0' # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] -gem "importmap-rails" +gem 'importmap-rails' # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] -gem "turbo-rails" +gem 'turbo-rails' # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] -gem "stimulus-rails" +gem 'stimulus-rails' # Build JSON APIs with ease [https://github.com/rails/jbuilder] -gem "jbuilder" +gem 'jbuilder' # Use Redis adapter to run Action Cable in production # gem "redis", "~> 4.0" @@ -37,10 +39,10 @@ gem "jbuilder" # gem "bcrypt", "~> 3.1.7" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem -gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ] +gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] # Reduces boot times through caching; required in config/boot.rb -gem "bootsnap", require: false +gem 'bootsnap', require: false # Use Sass to process CSS # gem "sassc-rails" @@ -48,26 +50,32 @@ gem "bootsnap", require: false # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] # gem "image_processing", "~> 1.2" +gem 'faraday' + group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem - gem "pry" + gem 'pry' end group :development do # Use console on exceptions pages [https://github.com/rails/web-console] - gem "web-console" + gem 'web-console' # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] # gem "rack-mini-profiler" # Speed up commands on slow machines / big apps [https://github.com/rails/spring] # gem "spring" - gem "rubocop-rails" + gem 'rubocop-rails' end group :test do - gem "rspec-rails" - gem "capybara" - gem "launchy" - gem "simplecov" -end \ No newline at end of file + gem 'capybara' + gem 'launchy' + gem 'orderly' + gem 'rspec-rails' + gem 'shoulda-matchers' + gem 'simplecov' + gem 'vcr' + gem 'webmock' +end diff --git a/Gemfile.lock b/Gemfile.lock index b54ee32ad..cf770dc32 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -69,6 +69,7 @@ GEM addressable (2.8.4) public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) + base64 (0.1.1) bindex (0.8.1) bootsnap (1.16.0) msgpack (~> 1.2) @@ -84,13 +85,21 @@ GEM xpath (~> 3.2) coderay (1.1.3) concurrent-ruby (1.2.2) + crack (0.4.5) + rexml crass (1.0.6) date (3.3.3) diff-lcs (1.5.0) docile (1.4.0) erubi (1.12.0) + faraday (2.7.11) + base64 + faraday-net_http (>= 2.0, < 3.1) + ruby2_keywords (>= 0.0.4) + faraday-net_http (3.0.2) globalid (1.1.0) activesupport (>= 5.0) + hashdiff (1.0.1) i18n (1.14.1) concurrent-ruby (~> 1.0) importmap-rails (1.2.1) @@ -131,6 +140,9 @@ GEM racc (~> 1.4) nokogiri (1.15.2-x86_64-darwin) racc (~> 1.4) + orderly (0.1.1) + capybara (>= 1.1) + rspec (>= 2.14) parallel (1.23.0) parser (3.2.2.3) ast (~> 2.4.1) @@ -178,6 +190,10 @@ GEM rake (13.0.6) regexp_parser (2.8.1) rexml (3.2.5) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) rspec-core (3.12.2) rspec-support (~> 3.12.0) rspec-expectations (3.12.3) @@ -213,6 +229,9 @@ GEM rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + shoulda-matchers (5.3.0) + activesupport (>= 5.2.0) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) @@ -237,11 +256,16 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) + vcr (6.2.0) web-console (4.2.0) actionview (>= 6.0.0) activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) + webmock (3.19.1) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -250,27 +274,33 @@ GEM zeitwerk (2.6.8) PLATFORMS + arm64-darwin-21 arm64-darwin-22 x86_64-darwin-21 DEPENDENCIES bootsnap capybara + faraday importmap-rails jbuilder launchy + orderly pg (~> 1.1) pry puma (~> 5.0) rails (~> 7.0.4, >= 7.0.4.2) rspec-rails rubocop-rails + shoulda-matchers simplecov sprockets-rails stimulus-rails turbo-rails tzinfo-data + vcr web-console + webmock RUBY VERSION ruby 3.2.2p53 diff --git a/Rakefile b/Rakefile index 9a5ea7383..488c551fe 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,8 @@ +# frozen_string_literal: true + # 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" +require_relative 'config/application' Rails.application.load_tasks diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 288b9ab71..fe9a18a95 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -13,3 +13,36 @@ *= require_tree . *= require_self */ + + #viewing_parties { + display: flex; + flex-wrap: wrap; + justify-content: space-around; +} + +.movie_section { + width: 500px; + padding: 5px; + margin: 10px; + outline-style: solid; + outline-color: grey; + outline-width: thick; +} + +.movie-list { + display: flex; + align-items: center; + justify-content: center; +} + +#average { + margin: 10px; +} + +#summary { + width: 500px; +} + +#reviews { + width: 700px; +} \ No newline at end of file diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb index d67269728..9aec23053 100644 --- a/app/channels/application_cable/channel.rb +++ b/app/channels/application_cable/channel.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ApplicationCable class Channel < ActionCable::Channel::Base end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 0ff5442f4..8d6c2a1bf 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ApplicationCable class Connection < ActionCable::Connection::Base end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 09705d12a..7944f9f99 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class ApplicationController < ActionController::Base end diff --git a/app/controllers/movies_controller.rb b/app/controllers/movies_controller.rb new file mode 100644 index 000000000..30221c9d2 --- /dev/null +++ b/app/controllers/movies_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class MoviesController < ApplicationController + def index; end + + def search + @movies = if params[:q] == 'top20rated' + facade.top_20_movies + else + facade.searched_movies(params[:q]) + end + end + + def show + @movie = facade.movie + @cast_members = facade.cast_members + @reviews = facade.reviews + end + + private + + def facade + MovieFacade.new(params[:movie_id]) + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 000000000..ce79ccab9 --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class UsersController < ApplicationController + def show + @user = User.find(params[:id]) + end + + def new; end + + def create + user = User.new(user_params) + if user.save + redirect_to root_path + elsif !user.save + flash[:error] = 'User email already in use, please enter another email' + redirect_to '/register/new' + end + end + + private + + def user_params + params.permit(:name, :email) + end +end diff --git a/app/controllers/viewing_parties_controller.rb b/app/controllers/viewing_parties_controller.rb new file mode 100644 index 000000000..f876ff726 --- /dev/null +++ b/app/controllers/viewing_parties_controller.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class ViewingPartiesController < ApplicationController + before_action :find_movie + before_action :find_user, only: %i[new create] + + def new + @users = User.where('id != ?', params[:id]) + end + + def create + viewing_party = ViewingParty.new(party_params) + + if viewing_party.duration < @movie.runtime + flash[:error] = 'Viewing party duration can not be less than the run time of the movie. Please try again.' + redirect_to "/users/#{params[:id]}/movies/#{@movie.id}/viewing_party/new" + else + viewing_party.save + UserViewingParty.create(user: @user, viewing_party:, host: true) + + User.all.each do |user| + UserViewingParty.create(user:, viewing_party:, host: false) if params[user.name.to_s] == '1' + end + + redirect_to "/users/#{params[:id]}" + end + end + + private + + def find_user + @user = User.find(params[:id]) + end + + def find_movie + @movie = facade.movie + end + + def party_params + params.permit(:movie_id, :duration, :day, :view_time) + end + + def facade + MovieFacade.new(params[:movie_id]) + end +end diff --git a/app/controllers/welcome_controller.rb b/app/controllers/welcome_controller.rb new file mode 100644 index 000000000..7f77f38fc --- /dev/null +++ b/app/controllers/welcome_controller.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class WelcomeController < ApplicationController + def index + @users = User.all + end +end diff --git a/app/facades/movie_facade.rb b/app/facades/movie_facade.rb new file mode 100644 index 000000000..68e360098 --- /dev/null +++ b/app/facades/movie_facade.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +class MovieFacade + def initialize(movie) + @movie = movie + end + + def cast_members + json = service.get_cast_members(@movie) + + json[:cast].map do |cast_data| + CastMember.new(cast_data) + end.first(10) + end + + def movie + json = service.get_movie(@movie) + + Movie.new(json) + end + + def reviews + json = service.get_reviews(@movie) + json[:results].map do |review_data| + Review.new(review_data) + end + end + + def top_20_movies + json = service.get_top_movies + + json[:results].map do |movie_data| + Movie.new(movie_data) + end.first(20) + end + + def searched_movies(query) + json = service.get_searched_movies(query) + + json[:results].map do |movie_data| + Movie.new(movie_data) + end.first(20) + end + + def service + MovieService.new + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de6be7945..15b06f0f6 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + module ApplicationHelper end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb new file mode 100644 index 000000000..4dc909ed6 --- /dev/null +++ b/app/helpers/users_helper.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +module UsersHelper +end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index d394c3d10..bef395997 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ApplicationJob < ActiveJob::Base # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 3c34c8148..d84cb6e71 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,4 +1,6 @@ +# frozen_string_literal: true + class ApplicationMailer < ActionMailer::Base - default from: "from@example.com" - layout "mailer" + default from: 'from@example.com' + layout 'mailer' end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index b63caeb8a..08dc53798 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ApplicationRecord < ActiveRecord::Base primary_abstract_class end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 000000000..bc00775ed --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class User < ApplicationRecord + has_many :user_viewing_parties + has_many :viewing_parties, through: :user_viewing_parties + validates :email, presence: true, uniqueness: true + validates :name, presence: true + + def parties_i_am_hosting + viewing_parties.joins(:user_viewing_parties) + .where('user_viewing_parties.host = ? AND user_viewing_parties.user_id = ?', true, id) + .distinct + end + + def parties_i_am_invited_to + viewing_parties.joins(:user_viewing_parties) + .where('user_viewing_parties.host = ? AND user_viewing_parties.user_id = ?', false, id) + .distinct + end +end diff --git a/app/models/user_viewing_party.rb b/app/models/user_viewing_party.rb new file mode 100644 index 000000000..d8801a785 --- /dev/null +++ b/app/models/user_viewing_party.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class UserViewingParty < ApplicationRecord + belongs_to :user + belongs_to :viewing_party +end diff --git a/app/models/viewing_party.rb b/app/models/viewing_party.rb new file mode 100644 index 000000000..f167da691 --- /dev/null +++ b/app/models/viewing_party.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class ViewingParty < ApplicationRecord + has_many :user_viewing_parties + has_many :users, through: :user_viewing_parties + validates :duration, presence: true + validates :day, presence: true + validates :view_time, presence: true + validates :movie_id, presence: true + + def find_movie + MovieFacade.new(movie_id).movie + end + + def find_host(current_user) + host_id = UserViewingParty.find_by('user_viewing_parties.host = true AND viewing_party_id = ?', id).user_id + + if current_user.id == host_id + 'Me' + else + User.find(host_id).name + end + end + + def party_guests + users.joins(:user_viewing_parties) + .select('users.*, user_viewing_parties.host') + .where('user_viewing_parties.host = ?', false) + .distinct + end +end diff --git a/app/poros/cast_member.rb b/app/poros/cast_member.rb new file mode 100644 index 000000000..a9285ecf3 --- /dev/null +++ b/app/poros/cast_member.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class CastMember + attr_reader :name, :character + + def initialize(data) + @name = data[:name] + @character = data[:character] + end +end diff --git a/app/poros/movie.rb b/app/poros/movie.rb new file mode 100644 index 000000000..24730f75d --- /dev/null +++ b/app/poros/movie.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class Movie + attr_reader :id, :title, :vote_average, :runtime, :genres, :overview, :backdrop_path + + def initialize(data) + @id = data[:id] + @backdrop_path = data[:backdrop_path] + @title = data[:title] + @vote_average = data[:vote_average] + @runtime = data[:runtime] + @genres = data[:genres] + @overview = data[:overview] + + def convert_time + if @runtime > 60 + "#{@runtime / 60}h #{@runtime % 60}min" + else + "#{@runtime}min" + end + end + + def format_genres + formatted = '' + @genres.map do |genre| + formatted += if genre != @genres.last + "#{genre[:name]}, " + else + genre[:name] + end + end + formatted + end + end +end diff --git a/app/poros/review.rb b/app/poros/review.rb new file mode 100644 index 000000000..72d3b7011 --- /dev/null +++ b/app/poros/review.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Review + attr_reader :author, :content + + def initialize(data) + @author = data[:author] + @content = data[:content] + end +end diff --git a/app/services/movie_service.rb b/app/services/movie_service.rb new file mode 100644 index 000000000..b01f09195 --- /dev/null +++ b/app/services/movie_service.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class MovieService + def get_movie(movie) + get_url("/3/movie/#{movie}") + end + + def get_cast_members(movie) + get_url("/3/movie/#{movie}/credits") + end + + def get_reviews(movie) + get_url("/3/movie/#{movie}/reviews") + end + + def get_top_movies + get_url('/3/movie/top_rated') + end + + def get_searched_movies(query) + get_url("3/search/movie?query=#{query}") + end + + def get_url(url) + response = conn.get(url) + JSON.parse(response.body, symbolize_names: true) + end + + def conn + Faraday.new(url: 'https://api.themoviedb.org') do |faraday| + faraday.params['api_key'] = Rails.application.credentials.tmdb[:key] + end + end +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 552042a39..2d9b279aa 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -10,7 +10,20 @@ <%= javascript_importmap_tags %> +
<%= link_to "Home", "/" %> | + +