diff --git a/Event Tracker video .mov b/Event Tracker video .mov new file mode 100644 index 00000000..2155b970 Binary files /dev/null and b/Event Tracker video .mov differ diff --git a/Gemfile b/Gemfile index c004f4ca..f2ddbc39 100644 --- a/Gemfile +++ b/Gemfile @@ -4,4 +4,6 @@ source "https://rubygems.org" gem "sinatra-activerecord" gem "sqlite3" gem "pry" +gem 'rake' gem "require_all" + diff --git a/Gemfile.lock b/Gemfile.lock index 9589226d..91e3b464 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,56 +1,58 @@ GEM remote: https://rubygems.org/ specs: - activemodel (6.0.3.1) - activesupport (= 6.0.3.1) - activerecord (6.0.3.1) - activemodel (= 6.0.3.1) - activesupport (= 6.0.3.1) - activesupport (6.0.3.1) + activemodel (6.0.3.4) + activesupport (= 6.0.3.4) + activerecord (6.0.3.4) + activemodel (= 6.0.3.4) + activesupport (= 6.0.3.4) + activesupport (6.0.3.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) zeitwerk (~> 2.2, >= 2.2.2) - coderay (1.1.1) - concurrent-ruby (1.1.6) - i18n (1.8.2) + coderay (1.1.3) + concurrent-ruby (1.1.7) + i18n (1.8.5) concurrent-ruby (~> 1.0) - method_source (0.8.2) - minitest (5.14.1) - mustermann (1.0.3) - pry (0.10.4) - coderay (~> 1.1.0) - method_source (~> 0.8.1) - slop (~> 3.4) + method_source (1.0.0) + minitest (5.14.2) + mustermann (1.1.1) + ruby2_keywords (~> 0.0.1) + pry (0.13.1) + coderay (~> 1.1) + method_source (~> 1.0) rack (2.2.3) - rack-protection (2.0.7) + rack-protection (2.1.0) rack - require_all (1.3.3) - sinatra (2.0.7) + rake (13.0.1) + require_all (3.0.0) + ruby2_keywords (0.0.2) + sinatra (2.1.0) mustermann (~> 1.0) - rack (~> 2.0) - rack-protection (= 2.0.7) + rack (~> 2.2) + rack-protection (= 2.1.0) tilt (~> 2.0) - sinatra-activerecord (2.0.12) - activerecord (>= 3.2) + sinatra-activerecord (2.0.21) + activerecord (>= 4.1) sinatra (>= 1.0) - slop (3.6.0) - sqlite3 (1.3.13) + sqlite3 (1.4.2) thread_safe (0.3.6) tilt (2.0.10) - tzinfo (1.2.7) + tzinfo (1.2.8) thread_safe (~> 0.1) - zeitwerk (2.3.0) + zeitwerk (2.4.2) PLATFORMS ruby DEPENDENCIES pry + rake require_all sinatra-activerecord sqlite3 BUNDLED WITH - 1.14.6 + 2.1.4 diff --git a/README.md b/README.md index b75f6185..b587e970 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,108 @@ Projects need to be approved prior to launching into them, so take some time to - Present any code you would like to highlight. 7. *OPTIONAL, BUT RECOMMENDED*: Write a blog post about the project and process. + + + + + +### Option One - Data Analytics Project +1. Access a Sqlite3 Database using ActiveRecord. + +2. You should have at minimum three models including one join model. This means you must have a many-to-many relationship. +User >--- Ticket ---< Event ---< Venue ---< City + +User has_many Tickets +User has_many Events through Tickets +Ticket belongs to one user +Ticket belongs to one event +Event has_one venue +Event has_one City through Venue +Event has many tickets +Event has many users through tickets +Venue has many events +Venue belongs to one city + +3. You should seed your database using data that you collect either from a CSV, a website by scraping, or an API. +Ticket master API +API Key: QATrioQ3vEzlLyBebumHRHuNBfT39vrZ +API call format: https://app.ticketmaster.com/{package}/{version}/{resource}.json?apikey=**{API key} + -package: discovery? + -version: v2? + -resources: event, attraction, classification, venue... + +4. Your models should have methods that answer interesting questions about the data. For example, if you've collected info about movie reviews, what is the most popular movie? What movie has the most reviews? +I want to see all events in my city +I want to see events near me on a certain day +I want to see if a certain artist or event is happening near me +I want to see what events are happening near me sorted by genre +I want to buy a ticket to an event +I want to see if the event I had a ticket for has been cancelled +I want to see the events I have tickets to + +5. You should provide a CLI to display the return values of your interesting methods. +Feature branches to build: + -search by date option + -see all genre options in my city, and pick one to see all events in that genre (1. Musicals, 2. NFL, 3. Country) + -method to call when search has no results + -find user when logging in + +6. Use good OO design patterns. You should have separate classes for your models and CLI interface. + + +User CLI options + -User starts by entering location + -takes a string of city and checks if valid in database + -if no, puts out "City not found" + -if yes, store city in args hash, start next method for date + + 0. search by attraction name. + 1. search by date MM/DD/YYYY + 0-9 reserved for results + -if 0-9 puts event details with y to buy and n to go back + F. filter option menu which puts out which keys to input for another filter + D. to enter a date into the search and filter results by it + P. to enter a price into the search and filter results by it + G. to enter and filter by genre + 2.search by price + 0-9 reserved for results + F. filter option menu which puts out which keys to input for another filter + D. to enter a date into the search and filter results by it + P. to enter a price into the search and filter results by it + G. to enter and filter by genre + 3. search by genre + 0-9 reserved for results + F. filter option menu which puts out which keys to input for another filter + D. to enter a date into the search and filter results by it + P. to enter a price into the search and filter results by it + G. to enter and filter by genre + + + + + -Entering B goes back to previous page + -See all Events available by date + -see all Events available by price range + -user sees list of events, can select one with 0-9 input + -sees event, venue, price, then Y/N to purchase ticket + -Y takes them back to main page + -N goes back one level + -purchase ticket for event + -See events they have tickets for + -see events they have tickets for by date + -See events they have that are not cancelled + + + + + --- ### Common Questions: - How do I turn off my SQL logger? ```ruby # in config/environment.rb add this line: ActiveRecord::Base.logger = nil + + + ``` diff --git a/app/date_search.rb b/app/date_search.rb new file mode 100644 index 00000000..b1ae9ff6 --- /dev/null +++ b/app/date_search.rb @@ -0,0 +1,10 @@ +class DateSearch + + def search_by_date + puts "Please enter a date: MM/DD/YYYY" + date = STDIN.gets.chomp.split("/") + date_formatted = "#{date[2]}-#{date[0]}-#{date[1]}" + Event.all.select {|e|e.date == date_formatted} + end + +end \ No newline at end of file diff --git a/app/genre_search.rb b/app/genre_search.rb new file mode 100644 index 00000000..ef00c42e --- /dev/null +++ b/app/genre_search.rb @@ -0,0 +1,44 @@ +class GenreSearch + def numbered_types(events) + i = 1 + numbered_types = {} + event_types = events.map{|event| event.event_type}.uniq + event_types.each do |type| + if type == "Undefined" + type = "Other" + numbered_types[i] = type + else + numbered_types[i] = type + end + i += 1 + end + numbered_types + end + + def genre_results(type, events) + if type == "Other" + type = "Undefined" + end + events_filtered_by_type = events.select {|event|event.event_type == type} + end + + def numbered_genres(events_filtered_by_type) + i = 1 + numbered_genres = {} + event_genres = events_filtered_by_type.map{|event| event.genre}.uniq + event_genres.each do |genre| + if genre == nil || genre == "Undefined" + genre = "Other" + numbered_genres[i] = genre + else + numbered_genres[i] = genre + end + i += 1 + end + numbered_genres + end + + + +end + diff --git a/app/menu.rb b/app/menu.rb new file mode 100644 index 00000000..b213c28c --- /dev/null +++ b/app/menu.rb @@ -0,0 +1,291 @@ +require_relative '../config/environment' +class Menu + attr_accessor :user + + def initialize + @user = user + end + + def start_program + puts + puts "Welcome to Event Tracker! This app will allow you to track your favorite events and see their current status." + self.user = get_user + pull_data_by_city_and_state(self.user.city, self.user.state) + puts + puts "Thank you, #{user.name}. Please select from the options below:" + begin_search + end + + def get_user_name + puts "Please enter your full name" + STDIN.gets.chomp + end + + def get_user_city + puts "Please enter your City" + input = STDIN.gets.chomp.split.map(&:capitalize).join(' ') + input == "" ? get_user_city : input + end + + def get_user_state + puts "Please enter your State (ex. WA, CA, NY)" + input = STDIN.gets.chomp.upcase + input == "" ? get_user_state : input + end + + def pull_data_by_city_and_state(city, state) + info = GetRequester.new("https://app.ticketmaster.com/discovery/v2/events.json?city=#{city}&stateCode=#{state}&size=100&sort=date,asc&apikey=QATrioQ3vEzlLyBebumHRHuNBfT39vrZ").parse_json + info["page"]["totalElements"] == 0 ? error_message : load_event_details(info) + end + + def get_user + puts "Enter 1 to Log in, or 2 to Create a new account, or x to exit the application" + input = STDIN.gets.chomp + if input == "1" + user = find_user_by(user_name = get_user_name) + if !user + puts "Account information not found. Would you like to make an account? Y / N " + STDIN.gets.chomp.downcase == "y" ? user = create_user_by(user_name, get_user_city, get_user_state) : back_to_start + end + elsif input == "2" + user = create_user_by(get_user_name, get_user_city, get_user_state) + elsif input == "x" + end_program + else + invalid_selection + start_program + end + user + end + + def find_user_by(name) + User.find_by(name: name) + end + + def create_user_by(name, city, state) + User.create(name: name, city: city, state: state) + end + + def load_event_details(info) + events = [] + info["_embedded"]["events"].each do |event| + new_event = Event.new + new_event.attraction_name = event.dig("name") + new_event.date = event.dig("dates", "start", "localDate") + new_event.venue = event.dig("_embedded", "venues", 0, "name") + new_event.genre = event.dig("classifications", 0, "genre", "name") + new_event.event_city = event.dig("_embedded", "venues", 0, "city", "name") + new_event.event_type = event.dig("classifications", 0, "segment", "name") + new_event.event_status = event.dig("dates", "status", "code") + new_event.event_state = event.dig("_embedded", "venues", 0, "state", "stateCode") + events << new_event + end + save_new_events(events) + end + + def save_new_events(events) + events.each do |event| + if Event.all.select {|e|e.attraction_name == event.attraction_name && e.date == event.date}.empty? + event.save + end + end + end + + def begin_search + puts + puts "1. Search by event name or artist name" + puts "2. Search by genre" + puts "3. Search by date" + puts "4. See all events in my city" + puts "5. See My Tracked Events and the Current Status" + puts "6. Change my city" + puts "Press 's' to log out of the app" + puts "Press 'x' to exit the app" + case STDIN.gets.chomp + when "1" + display_results_by_attraction_name + when "2" + get_results_by_event_type + when "3" + display_results_by_date + when "4" + display_results_in_users_city + when "5" + self.user.display_tracked_events + when "6" + change_user_city + when "s" + back_to_start + when "x" + end_program + else + invalid_selection + end + begin_search + end + + def filter_events_by_user_city(events) + events.select {|e|e.event_city == self.user.city && e.event_state == self.user.state} + end + + def display_results_by_attraction_name + events = filter_events_by_user_city(NameSearch.new.results) + events.empty? ? no_results_found : display_events(events) + end + + def display_results_by_date + events = filter_events_by_user_city(DateSearch.new.search_by_date) + events.empty? ? no_results_found : display_events(events) + end + + def display_results_in_users_city + events = Event.all.select {|e|e.event_city == self.user.city && e.event_state == self.user.state} + display_events(events) + end + + def display_events(events) + puts "Here are the events available:" + i = 1 + events = events.sort_by(&:date) + events.each do |e| + puts "#{i}. " + e.event_display_format + i = i+1 + end + track_event(events) + end + + def track_event(events) + puts "Would you like to track an event's status? Enter the number of the event, or x to go back" + input = STDIN.gets.chomp + begin_search if input == "x" + if (input.match? /\A\d+\z/) && (input.to_i <= events.length) + self.user.confirm_track_event(events[input.to_i-1]) + begin_search + else + invalid_selection + display_events(events) + end + end + + def change_user_city + self.user.change_city + pull_data_by_city_and_state(self.user.city, self.user.state) + end + + def no_results_found + puts + puts "No results found. Please try again" + puts + begin_search + end + + def invalid_selection + puts + puts "Not a valid selection. Please try again." + puts + end + + + def back_to_start + Menu.new.start_program + end + + def end_program + puts "Goodbye!" + exit + end + + def get_results_by_event_type + events = Event.all.select {|e|e.event_city == self.user.city && e.event_state == self.user.state} + numbered_types = GenreSearch.new.numbered_types(events) + if events.empty? + no_results_found + begin_search + else + user_select_event_type(numbered_types, events) + end + end + + + def user_select_event_type(numbered_types, events) + puts "Please select the number of the event type you would like to see:" + numbered_types.each {|num, type| puts "#{num}. #{type}"} + puts "Press 's' to log out of the app" + puts "Press 'x' to exit the app" + user_input = STDIN.gets.chomp.downcase + if numbered_types[user_input.to_i] + type = numbered_types[user_input.to_i] + results_by_genre(type, events) + elsif user_input == "s" + back_to_start + elsif user_input == "x" + end_program + else + puts + invalid_selection + puts + user_select_event_type(numbered_types, events) + end + end + + def results_by_genre(type, events) + events_filtered_by_type = GenreSearch.new.genre_results(type, events) + if events_filtered_by_type.empty? + no_results_found + begin_search + end + user_select_genre(GenreSearch.new.numbered_genres(events_filtered_by_type)) + end + + def user_select_genre(numbered_genres) + puts "Please select the number of the genre you would like to see:" + numbered_genres.each {|num, genre| puts "#{num}. #{genre}"} + puts "Press 's' to log out of the app" + puts "Press 'x' to exit the app" + user_input = STDIN.gets.chomp.downcase + if numbered_genres[user_input.to_i] + genre = numbered_genres[user_input.to_i] + display_genre_events(genre) + elsif user_input == "s" + back_to_start + elsif user_input == "x" + end_program + else + puts + invalid_selection + puts + user_select_genre(numbered_genres) + end + + end + + def display_genre_events(genre) + events = Event.all.select {|e|e.event_city == self.user.city && e.event_state == self.user.state} + if genre == "Other" + events_by_genre = events.select {|event|event.genre == nil || event.genre == "Undefined"} + display_events(events_by_genre) + else + events_by_genre = events.select {|event|event.genre == genre} + display_events(events_by_genre) + end + end + + def error_message + puts + puts "No events found in your city :(" + puts + puts "Press '1' to enter a new city." + puts "Press 'x' to exit the program." + user_input = STDIN.gets.chomp + if user_input == "1" + change_user_city + elsif user_input == "x" + self.user.delete + end_program + else + puts "Invalid entry, please try another option" + end + end + +end + diff --git a/app/models/event.rb b/app/models/event.rb new file mode 100644 index 00000000..1c0993bb --- /dev/null +++ b/app/models/event.rb @@ -0,0 +1,13 @@ +class Event < ActiveRecord::Base + has_many :tickets + has_many :users, through: :tickets + + def date_display_format + date = self.date.split("-") + "#{Date::MONTHNAMES[date[1].to_i]} #{date[2].to_i}, #{date[0].to_i}" + end + + def event_display_format + "#{self.attraction_name}. Current status: #{self.event_status.capitalize}. Scheduled for #{self.date_display_format}, at #{self.venue}." + end +end \ No newline at end of file diff --git a/app/models/ticket.rb b/app/models/ticket.rb new file mode 100644 index 00000000..b318a2f6 --- /dev/null +++ b/app/models/ticket.rb @@ -0,0 +1,4 @@ +class Ticket < ActiveRecord::Base + belongs_to :user + belongs_to :event +end \ No newline at end of file diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 00000000..abb229c1 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,68 @@ +class User < ActiveRecord::Base + has_many :tickets + has_many :events, through: :tickets + + def my_upcoming_events + self.tickets.all.map {|t|t.event}.sort_by(&:date) + end + + def display_user_information + puts self.name + puts "#{self.city}, #{self.state}" + press_any_key_to_go_back + end + + def display_tracked_events + self.my_upcoming_events.empty? ? display_has_no_saved_events : show_events + press_any_key_to_go_back + end + + def show_events + puts + puts "Here are your saved events:" + self.my_upcoming_events.each {|event|puts event_display_format(event)} + end + + def display_has_no_saved_events + puts + puts "You are not tracking any events at this time" + puts + end + + def confirm_track_event(event) + puts event_display_format(event) + puts "Confirm you would like to track this event. Y or N." + y_n_input = STDIN.gets.chomp.downcase + if y_n_input == "y" + Ticket.create(user_id: self.id, event_id: event.id) + puts + puts "Now tracking event: " + puts event_display_format(event) + puts + elsif y_n_input == "n" + puts "Returning to search results..." + puts + else + puts "Not a valid selection. Please enter Y or N:" + puts + confirm_track_event(event) + end + end + + def event_display_format(event) + "#{event.attraction_name}. Current status: #{event.event_status.capitalize}. Scheduled for #{event.date_display_format}, at #{event.venue}." + end + + def change_city + puts "Please enter your city:" + self.city = STDIN.gets.chomp.split.map(&:capitalize).join(' ') + puts "Please enter your State: (WA, CA, FL...)" + self.state = STDIN.gets.chomp.upcase + end + + def press_any_key_to_go_back + puts + puts "Press Enter to continue." + gets + end +end \ No newline at end of file diff --git a/app/name_search.rb b/app/name_search.rb new file mode 100644 index 00000000..be6beadb --- /dev/null +++ b/app/name_search.rb @@ -0,0 +1,14 @@ +class NameSearch + + def results + puts "Please enter the event or artist you would like to see:" + user_input = STDIN.gets.chomp.downcase.split(" ") + events = [] + user_input.each do |word| + found_events = Event.all.select {|event|event.attraction_name.split.any?(word.capitalize) || event.attraction_name.split.any?(word)}.uniq + found_events.each {|e| events << e} + end + events.uniq + end + +end \ No newline at end of file diff --git a/bin/run.rb b/bin/run.rb index cf08c338..4d4cd344 100644 --- a/bin/run.rb +++ b/bin/run.rb @@ -1,5 +1,2 @@ require_relative '../config/environment' - - - -puts "HELLO WORLD" +Menu.new.start_program diff --git a/config/environment.rb b/config/environment.rb index 4dbe13e5..ced65e9b 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -3,3 +3,4 @@ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: 'db/development.db') require_all 'lib' +require_all 'app' diff --git a/db/migrate/20201203173043_create_users.rb b/db/migrate/20201203173043_create_users.rb new file mode 100644 index 00000000..fca8d0bf --- /dev/null +++ b/db/migrate/20201203173043_create_users.rb @@ -0,0 +1,8 @@ +class CreateUsers < ActiveRecord::Migration[6.0] + def change + create_table :users do |t| + t.string :name + t.string :city + end + end +end diff --git a/db/migrate/20201203173054_create_tickets.rb b/db/migrate/20201203173054_create_tickets.rb new file mode 100644 index 00000000..905c1a12 --- /dev/null +++ b/db/migrate/20201203173054_create_tickets.rb @@ -0,0 +1,8 @@ +class CreateTickets < ActiveRecord::Migration[6.0] + def change + create_table :tickets do |t| + t.integer :user_id + t.integer :event_id + end + end +end diff --git a/db/migrate/20201203173109_create_events.rb b/db/migrate/20201203173109_create_events.rb new file mode 100644 index 00000000..5514b002 --- /dev/null +++ b/db/migrate/20201203173109_create_events.rb @@ -0,0 +1,12 @@ +class CreateEvents < ActiveRecord::Migration[6.0] + def change + create_table :events do |t| + t.string :attraction_name + t.string :date + t.string :venue + t.string :genre + t.float :price_min + t.float :price_max + end + end +end diff --git a/db/migrate/20201203192904_add_status_to_events.rb b/db/migrate/20201203192904_add_status_to_events.rb new file mode 100644 index 00000000..9a785f65 --- /dev/null +++ b/db/migrate/20201203192904_add_status_to_events.rb @@ -0,0 +1,5 @@ +class AddStatusToEvents < ActiveRecord::Migration[6.0] + def change + add_column :events, :event_status, :string + end +end diff --git a/db/migrate/20201204000105_add_state_to_users.rb b/db/migrate/20201204000105_add_state_to_users.rb new file mode 100644 index 00000000..22d5c4a5 --- /dev/null +++ b/db/migrate/20201204000105_add_state_to_users.rb @@ -0,0 +1,5 @@ +class AddStateToUsers < ActiveRecord::Migration[6.0] + def change + add_column :users, :state, :string + end +end diff --git a/db/migrate/20201204184104_add_city_and_event_type_to_events.rb b/db/migrate/20201204184104_add_city_and_event_type_to_events.rb new file mode 100644 index 00000000..23a952e9 --- /dev/null +++ b/db/migrate/20201204184104_add_city_and_event_type_to_events.rb @@ -0,0 +1,6 @@ +class AddCityAndEventTypeToEvents < ActiveRecord::Migration[6.0] + def change + add_column :events, :event_city, :string + add_column :events, :event_type, :string + end +end diff --git a/db/migrate/20201207194651_add_state_to_events.rb b/db/migrate/20201207194651_add_state_to_events.rb new file mode 100644 index 00000000..9a14fc0c --- /dev/null +++ b/db/migrate/20201207194651_add_state_to_events.rb @@ -0,0 +1,5 @@ +class AddStateToEvents < ActiveRecord::Migration[6.0] + def change + add_column :events, :event_state, :string + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 00000000..32d35f41 --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,39 @@ +# 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 `rails +# db:schema:load`. When creating a new database, `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.define(version: 2020_12_07_194651) do + + create_table "events", force: :cascade do |t| + t.string "attraction_name" + t.string "date" + t.string "venue" + t.string "genre" + t.float "price_min" + t.float "price_max" + t.string "event_status" + t.string "event_city" + t.string "event_type" + t.string "event_state" + end + + create_table "tickets", force: :cascade do |t| + t.integer "user_id" + t.integer "event_id" + end + + create_table "users", force: :cascade do |t| + t.string "name" + t.string "city" + t.string "state" + end + +end diff --git a/lib/get_requester.rb b/lib/get_requester.rb new file mode 100644 index 00000000..1afb4aa1 --- /dev/null +++ b/lib/get_requester.rb @@ -0,0 +1,19 @@ +require 'open-uri' +require 'net/http' +require 'json' + +class GetRequester + def initialize(url) + @url = url + end + + def get_response_body + uri = URI.parse(@url) + response = Net::HTTP.get_response(uri) + response.body + end + + def parse_json + JSON.parse(self.get_response_body) + end +end \ No newline at end of file