diff --git a/.gitignore b/.gitignore index 18b43c9cd2..c93b66abd8 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ # Ignore master key for decrypting credentials and more. /config/master.key +/coverage +# Ignore application configuration +/config/application.yml diff --git a/Gemfile b/Gemfile index 2e86ac3e16..dfd309f605 100644 --- a/Gemfile +++ b/Gemfile @@ -23,7 +23,7 @@ gem 'jbuilder', '~> 2.5' # Use Redis adapter to run Action Cable in production # gem 'redis', '~> 4.0' # Use ActiveModel has_secure_password -# gem 'bcrypt', '~> 3.1.7' +gem 'bcrypt', '~> 3.1.7' # Use ActiveStorage variant # gem 'mini_magick', '~> 4.8' @@ -34,25 +34,39 @@ gem 'jbuilder', '~> 2.5' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', '>= 1.1.0', require: false +gem 'faraday' + +gem 'email_validator' + +gem 'bootstrap-sass', '~> 3.2.0' + +gem 'jquery-rails' + group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console + gem 'figaro' gem 'pry' end group :development do # Access an interactive console on exception pages or by calling 'console' anywhere in the code. - gem 'web-console', '>= 3.3.0' gem 'listen', '>= 3.0.5', '< 3.2' gem 'rubocop-rails' + gem 'web-console', '>= 3.3.0' end group :test do - gem 'rspec-rails' gem 'capybara' + gem 'database_cleaner' + gem 'factory_bot_rails', '~> 4.0' + gem 'faker' gem 'launchy' + gem 'orderly' + gem 'rspec-rails' + gem 'shoulda-matchers', '~> 3.1' gem 'simplecov' + gem 'webmock' end - # Windows does not include zoneinfo files, so bundle the tzinfo-data gem -gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] +gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] diff --git a/Gemfile.lock b/Gemfile.lock index c8ab2fd7c1..106c4bf2a2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -46,9 +46,12 @@ GEM public_suffix (>= 2.0.2, < 5.0) arel (9.0.0) ast (2.4.2) + bcrypt (3.1.18) bindex (0.8.1) bootsnap (1.9.1) msgpack (~> 1.0) + bootstrap-sass (3.2.0.4) + sass (~> 3.2) builder (3.2.4) capybara (3.36.0) addressable @@ -68,18 +71,46 @@ GEM execjs coffee-script-source (1.12.2) concurrent-ruby (1.1.9) + crack (0.4.5) + rexml crass (1.0.6) + database_cleaner (2.0.1) + database_cleaner-active_record (~> 2.0.0) + database_cleaner-active_record (2.0.1) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0.0) + database_cleaner-core (2.0.1) diff-lcs (1.4.4) docile (1.4.0) + email_validator (2.2.4) + activemodel erubi (1.10.0) execjs (2.8.1) + factory_bot (4.11.1) + activesupport (>= 3.0.0) + factory_bot_rails (4.11.1) + factory_bot (~> 4.11.1) + railties (>= 3.0.0) + faker (3.1.0) + i18n (>= 1.8.11, < 2) + faraday (2.7.4) + faraday-net_http (>= 2.0, < 3.1) + ruby2_keywords (>= 0.0.4) + faraday-net_http (3.0.2) ffi (1.15.4) + figaro (1.2.0) + thor (>= 0.14.0, < 2) globalid (0.5.2) activesupport (>= 5.0) + hashdiff (1.0.1) i18n (1.8.11) concurrent-ruby (~> 1.0) jbuilder (2.11.3) activesupport (>= 5.0.0) + jquery-rails (4.5.1) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) launchy (2.5.0) addressable (~> 2.7) listen (3.1.5) @@ -104,6 +135,9 @@ GEM racc (~> 1.4) nokogiri (1.12.5-arm64-darwin) racc (~> 1.4) + orderly (0.1.1) + capybara (>= 1.1) + rspec (>= 2.14) parallel (1.21.0) parser (3.0.2.0) ast (~> 2.4.1) @@ -148,6 +182,10 @@ GEM ffi (~> 1.0) regexp_parser (2.1.1) rexml (3.2.5) + rspec (3.10.0) + rspec-core (~> 3.10.0) + rspec-expectations (~> 3.10.0) + rspec-mocks (~> 3.10.0) rspec-core (3.10.1) rspec-support (~> 3.10.0) rspec-expectations (3.10.1) @@ -181,6 +219,7 @@ GEM rack (>= 1.1) rubocop (>= 1.7.0, < 2.0) ruby-progressbar (1.11.0) + ruby2_keywords (0.0.5) ruby_dep (1.5.0) sass (3.7.4) sass-listen (~> 4.0.0) @@ -193,6 +232,8 @@ GEM sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) + shoulda-matchers (3.1.3) + activesupport (>= 4.0.0) simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) @@ -219,6 +260,10 @@ GEM activemodel (>= 5.0) bindex (>= 0.4.0) railties (>= 5.0) + webmock (3.18.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) @@ -230,12 +275,22 @@ PLATFORMS ruby DEPENDENCIES + bcrypt (~> 3.1.7) bootsnap (>= 1.1.0) + bootstrap-sass (~> 3.2.0) capybara coffee-rails (~> 4.2) + database_cleaner + email_validator + factory_bot_rails (~> 4.0) + faker + faraday + figaro jbuilder (~> 2.5) + jquery-rails launchy listen (>= 3.0.5, < 3.2) + orderly pg (>= 0.18, < 2.0) pry puma (~> 3.11) @@ -243,10 +298,12 @@ DEPENDENCIES rspec-rails rubocop-rails sass-rails (~> 5.0) + shoulda-matchers (~> 3.1) simplecov tzinfo-data uglifier (>= 1.3.0) web-console (>= 3.3.0) + webmock RUBY VERSION ruby 2.7.4p191 diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 43ba7e9701..2967dafe64 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -13,3 +13,6 @@ //= require rails-ujs //= require activestorage //= require_tree . + +//= require jquery +//= require bootstrap-sprockets diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.scss similarity index 92% rename from app/assets/stylesheets/application.css rename to app/assets/stylesheets/application.scss index d05ea0f511..092592b5ed 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.scss @@ -13,3 +13,6 @@ *= require_tree . *= require_self */ + + @import "bootstrap-sprockets"; + @import "bootstrap"; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 09705d12ab..9c000d85d1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,2 +1,14 @@ class ApplicationController < ActionController::Base + helper_method :current_user + + def current_user + @user ||= User.find(session[:user_id]) if session[:user_id] + end + + def validate_user + unless current_user + flash[:error] = ['User must be logged in'] + redirect_to root_path + end + end end diff --git a/app/controllers/discover_controller.rb b/app/controllers/discover_controller.rb new file mode 100644 index 0000000000..338d6706b1 --- /dev/null +++ b/app/controllers/discover_controller.rb @@ -0,0 +1,4 @@ +class DiscoverController < ApplicationController + def index + end +end \ No newline at end of file diff --git a/app/controllers/movies_controller.rb b/app/controllers/movies_controller.rb new file mode 100644 index 0000000000..6d96dde7b8 --- /dev/null +++ b/app/controllers/movies_controller.rb @@ -0,0 +1,26 @@ +class MoviesController < ApplicationController + def index + if params[:q] == 'top rated' + + @movies = MovieFacade.find_movies("discover/movie?api_key=#{ENV['movie_api_key']}&language=en-US&sort_by=vote_average.desc&include_adult=false&include_video=false&page=1&vote_count.gte=1000") + + elsif params[:commit] + + @movies = MovieFacade.find_movies("search/movie?api_key=#{ENV['movie_api_key']}&language=en-US&query=#{params[:title]}&page=1&include_adult=false") + + end + + if @movies.is_a?(Hash) && (@movies[:success] == false) + flash[:error] = @movies[:errors] + redirect_to discover_index_path + end + end + + def show + @movie = MovieFacade.find_movie(params[:id]) + + @cast = MovieFacade.find_cast(params[:id]) + + @reviews = MovieFacade.find_reviews(params[:id]) + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000000..925c956aef --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,21 @@ +class SessionsController < ApplicationController + def new + + end + + def create + user = User.find_by(email: params[:email]) + if user&.authenticate(params[:password]) + session[:user_id] = user.id + redirect_to user_path(user) + else + flash[:error] = ['Login Failed'] + render :new + end + end + + def delete + session.clear + redirect_to root_path + end +end \ No newline at end of file diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 0000000000..44b4a54265 --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,27 @@ +class UsersController < ApplicationController + def new + @user = User.new + end + + def create + user = User.new(user_params) + + if user.save + session[:user_id] = user.id + redirect_to user_path(user) + else + flash[:error] = user.errors.full_messages + render :new + end + end + + def show + validate_user + end + + private + + def user_params + params.require(:user).permit(:name, :email, :password, :password_confirmation) + end +end diff --git a/app/controllers/viewing_parties_controller.rb b/app/controllers/viewing_parties_controller.rb new file mode 100644 index 0000000000..3d0a93552c --- /dev/null +++ b/app/controllers/viewing_parties_controller.rb @@ -0,0 +1,41 @@ +class ViewingPartiesController < ApplicationController + def new + @viewing_party = ViewingParty.new + if @user = current_user + @users = @user.all_but_self + @movie = MovieFacade.find_movie(params[:movie_id]) + else + flash[:error] = ['User must be logged in'] + redirect_to movie_path(params[:movie_id]) + end + end + + def create + @viewing_party = ViewingParty.new(viewing_party_params) + + if @viewing_party.save + redirect_to user_path(@user) + else + flash[:error] = @viewing_party.errors.full_messages + redirect_to new_movie_viewing_party_path(params[:movie_id]) + end + + create_user_viewing_parties + end + + private + + def create_user_viewing_parties + UserViewingParty.create({ user_id: current_user.id, viewing_party_id: @viewing_party.id, hosting: true }) + params[:viewing_party].each do |key, value| + if value == '1' && key != :movie_id + UserViewingParty.create({ user_id: key, viewing_party_id: @viewing_party.id }) + end + end + end + + def viewing_party_params + params[:viewing_party][:movie_id] = params[:movie_id] + params.require(:viewing_party).permit(:duration, :start_time, :date, :movie_id) + end +end diff --git a/app/controllers/welcome_controller.rb b/app/controllers/welcome_controller.rb new file mode 100644 index 0000000000..ab4d827961 --- /dev/null +++ b/app/controllers/welcome_controller.rb @@ -0,0 +1,5 @@ +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 0000000000..27f1e20703 --- /dev/null +++ b/app/facades/movie_facade.rb @@ -0,0 +1,39 @@ +class MovieFacade + def self.find_movie(movie_id) + @movie = Movie.new(filter_movies("movie/#{movie_id}?api_key=#{ENV['movie_api_key']}&language=en-US")) + end + + def self.find_cast(movie_id) + @cast = filter_credits("movie/#{movie_id}/credits?api_key=#{ENV['movie_api_key']}&language=en-US").map do |character| + Character.new(character) + end + end + + def self.find_reviews(movie_id) + @reviews = filter_response("movie/#{movie_id}/reviews?api_key=#{ENV['movie_api_key']}&language=en-US").map do |review| + Review.new(review) + end + end + + def self.find_movies(uri) + if filter_movies(uri)[:success] == false + filter_movies(uri) + else + filter_movies(uri)[:results].map do |movie| + Movie.new(movie) + end + end + end + + def self.filter_movies(uri) + MovieService.parse_response(uri) + end + + def self.filter_response(uri) + MovieService.parse_response(uri)[:results] + end + + def self.filter_credits(uri) + MovieService.parse_response(uri)[:cast][0..9] + end +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000000..d497459f22 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,18 @@ +class User < ApplicationRecord + has_many :user_viewing_parties + has_many :viewing_parties, through: :user_viewing_parties + + validates_presence_of :name, :email, :password + validates :email, uniqueness: { case_sensitive: false, message: 'User already exists with given email' } + validates :email, email: true + + has_secure_password + + def find_user_viewing_party(viewing_party) + user_viewing_parties.find_by(viewing_party_id: viewing_party.id) + end + + def all_but_self + User.where.not(id: id) + end +end diff --git a/app/models/user_viewing_party.rb b/app/models/user_viewing_party.rb new file mode 100644 index 0000000000..b8ab9167d1 --- /dev/null +++ b/app/models/user_viewing_party.rb @@ -0,0 +1,6 @@ +class UserViewingParty < ApplicationRecord + belongs_to :viewing_party + belongs_to :user + + validates_inclusion_of :hosting, in: [true, false] +end diff --git a/app/models/viewing_party.rb b/app/models/viewing_party.rb new file mode 100644 index 0000000000..7f6283e30a --- /dev/null +++ b/app/models/viewing_party.rb @@ -0,0 +1,20 @@ +class ViewingParty < ApplicationRecord + has_many :user_viewing_parties + has_many :users, through: :user_viewing_parties + + validates_numericality_of :duration + validates_presence_of :date, :duration, :movie_id, :start_time + validates_with DurationValidator + + def movie + MovieFacade.find_movie(movie_id) if movie_id + end + + def host + user_viewing_parties.find_by(hosting: true).user.name + end + + def invitees + user_viewing_parties.filter_map { |uvp| uvp.user.name unless uvp.hosting } + end +end diff --git a/app/poros/character.rb b/app/poros/character.rb new file mode 100644 index 0000000000..04577f95a9 --- /dev/null +++ b/app/poros/character.rb @@ -0,0 +1,8 @@ +class Character + attr_reader :actor, :character + + def initialize(data) + @actor = data[:name] + @character = data[:character] + end +end diff --git a/app/poros/movie.rb b/app/poros/movie.rb new file mode 100644 index 0000000000..03834e0c6b --- /dev/null +++ b/app/poros/movie.rb @@ -0,0 +1,29 @@ +class Movie + attr_reader :id, + :title, + :vote_average, + :runtime, + :genres, + :hours_mins, + :image, + :overview + + def initialize(data) + @id = data[:id] + @title = data[:title] + @vote_average = data[:vote_average] + @runtime = data[:runtime] + @overview = data[:overview] + @hours_mins = convert_runtime(data[:runtime]) if data[:runtime] + @genres = convert_genres(data[:genres]) if data[:genres] + @image = data[:poster_path] + end + + def convert_runtime(runtime) + "#{runtime / 60}hr #{runtime % 60}min" + end + + def convert_genres(genres) + genres.map { |genre| genre[:name] } + end +end diff --git a/app/poros/review.rb b/app/poros/review.rb new file mode 100644 index 0000000000..5ae8cc9b64 --- /dev/null +++ b/app/poros/review.rb @@ -0,0 +1,12 @@ +class Review + attr_reader :author, :content + + def initialize(data) + @author = data[:author] + @content = content_strip(data[:content]) + end + + def content_strip(content) + content.gsub('’', "'") + end +end diff --git a/app/services/movie_service.rb b/app/services/movie_service.rb new file mode 100644 index 0000000000..151e76124e --- /dev/null +++ b/app/services/movie_service.rb @@ -0,0 +1,13 @@ +class MovieService + def self.connection + Faraday.new(url: 'https://api.themoviedb.org/3') + end + + def self.parse_response(uri) + JSON.parse(response(uri).body, symbolize_names: true) + end + + def self.response(uri) + connection.get(uri) + end +end diff --git a/app/validators/duration_validator.rb b/app/validators/duration_validator.rb new file mode 100644 index 0000000000..747948ee9e --- /dev/null +++ b/app/validators/duration_validator.rb @@ -0,0 +1,7 @@ +class DurationValidator < ActiveModel::Validator + def validate(record) + if record.movie_id && (record.duration < record.movie.runtime) + record.errors.add :base, 'Duration must be greater than or equal to movie runtime' + end + end +end diff --git a/app/views/discover/index.html.erb b/app/views/discover/index.html.erb new file mode 100644 index 0000000000..4243900d4e --- /dev/null +++ b/app/views/discover/index.html.erb @@ -0,0 +1,13 @@ +