diff --git a/Gemfile b/Gemfile index 1f478929..cc9b3f83 100644 --- a/Gemfile +++ b/Gemfile @@ -3,3 +3,6 @@ ruby '2.0.0' gem 'rspec', '~> 2.14.1' gem 'pry-byebug' +gem 'pg' +gem 'sinatra' +gem 'rake' diff --git a/Gemfile.lock b/Gemfile.lock index a885a8c7..8da63939 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,6 +10,7 @@ GEM debugger-linecache (1.2.0) diff-lcs (1.2.5) method_source (0.8.2) + pg (0.17.1) pry (0.10.1) coderay (~> 1.1.0) method_source (~> 0.8.1) @@ -17,6 +18,10 @@ GEM pry-byebug (2.0.0) byebug (~> 3.4) pry (~> 0.10) + rack (1.5.2) + rack-protection (1.5.3) + rack + rake (10.3.2) rspec (2.14.1) rspec-core (~> 2.14.0) rspec-expectations (~> 2.14.0) @@ -25,11 +30,19 @@ GEM rspec-expectations (2.14.5) diff-lcs (>= 1.1.3, < 2.0) rspec-mocks (2.14.5) + sinatra (1.4.5) + rack (~> 1.4) + rack-protection (~> 1.4) + tilt (~> 1.3, >= 1.3.4) slop (3.6.0) + tilt (1.4.1) PLATFORMS ruby DEPENDENCIES + pg pry-byebug + rake rspec (~> 2.14.1) + sinatra diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..5f19a6ca --- /dev/null +++ b/Rakefile @@ -0,0 +1,50 @@ + +task :environment do + require './lib/songify.rb' +end + +task :console => :environment do + require 'irb' + ARGV.clear + IRB.start +end + + +namespace :db do + + task :create do + `createdb songify_test` + `createdb songify_dev` + puts "Created." + end + + task :drop do + `dropdb songify_test` + `dropdb songify_dev` + puts "Dropped." + end + + task :create_tables => :environment do + db1 = Songify.create_db_connection('songify_test') + db2 = Songify.create_db_connection('songify_dev') + Songify.create_tables(db1) + Songify.create_tables(db2) + puts "Created tables." + end + + task :drop_tables => :environment do + db1 = Songify.create_db_connection('songify_test') + db2 = Songify.create_db_connection('songify_dev') + Songify.drop_tables(db1) + Songify.drop_tables(db2) + puts "Dropped tables." + end + + task :clear => :environment do + # The test db clears all the time, so there's no point in doing it here. + db = Songify.create_db_connection('songify_dev') + Songify.drop_tables(db) + puts "Cleared tables." + end + +end diff --git a/lib/songify.rb b/lib/songify.rb new file mode 100644 index 00000000..2fc0c855 --- /dev/null +++ b/lib/songify.rb @@ -0,0 +1,55 @@ +require 'pg' + +module Songify + def self.create_db_connection(dbname) + PG.connect(host: 'localhost', dbname: dbname) + end + + def self.clear_db(db) + db.exec <<-SQL + DELETE FROM album_genres; + DELETE FROM songs; + DELETE FROM albums; + DELETE FROM genres; + /* TODO: Clear rest of the tables (books, etc.) */ + SQL + end + + def self.create_tables(db) + db.exec <<-SQL + CREATE TABLE IF NOT EXISTS albums( + id SERIAL PRIMARY KEY, + title VARCHAR + ); + CREATE TABLE IF NOT EXISTS songs( + id SERIAL PRIMARY KEY, + album_id integer REFERENCES albums (id), + title VARCHAR + ); + CREATE TABLE IF NOT EXISTS genres( + id SERIAL PRIMARY KEY, + name VARCHAR + ); + /* TODO: Create song_genres table */ + CREATE TABLE IF NOT EXISTS album_genres( + id SERIAL PRIMARY KEY, + album_id integer REFERENCES albums (id), + genre_id integer REFERENCES genres (id) + ); + SQL + end + + def self.drop_tables(db) + db.exec <<-SQL + DROP TABLE album_genres; + DROP TABLE songs; + DROP TABLE albums; + DROP TABLE genres; + /* TODO: Drop song_genres table */ + SQL + end +end + +require_relative 'songify/album_repo' +require_relative 'songify/genre_repo' +require_relative 'songify/song_repo' diff --git a/lib/songify/album_repo.rb b/lib/songify/album_repo.rb new file mode 100644 index 00000000..17f58788 --- /dev/null +++ b/lib/songify/album_repo.rb @@ -0,0 +1,56 @@ +require 'pry-byebug' + +module Songify + class AlbumRepo + + def self.add_genres(db, album_data) + album_id = album_data['id'] + album_data['genre_ids'].each do |genre_id| + db.exec("INSERT INTO album_genres (album_id, genre_id) VALUES ($1, $2)", [album_id, genre_id]) + end + end + + def self.all(db) + db.exec("SELECT * FROM albums").to_a + end + + def self.find(db, album_id) + genres = [] + result = db.exec("SELECT * FROM albums WHERE id = $1", [album_id]).first + if result + album_genres = db.exec("SELECT * FROM album_genres a JOIN genres g ON g.id = a.genre_id WHERE a.album_id = $1", [album_id]).to_a + album_genres.each do |line| + genres << {'id' => line['genre_id'], 'name' => line['name']} + end + result['genres'] = genres + end + result + end + + def self.save(db, album_data) + if album_data['id'] + result = db.exec("UPDATE albums SET title = $2 WHERE id = $1", [album_data['id'], album_data['title']]) + self.find(db, album_data['id']) + elsif album_data['genre_ids'] + album_id = db.exec("INSERT INTO albums (title) VALUES ($1) RETURNING id", [album_data['title']]).first + album_data['id'] = album_id['id'] + album_data['genre_ids'].each do |genre| + db.exec("INSERT INTO album_genres (album_id, genre_id) VALUES ($1, $2)", [album_id['id'], genre]) + end + album_id + else + raise "title is required." if album_data['title'].nil? || album_data['title'] == '' + result = db.exec("INSERT INTO albums (title) VALUES ($1) RETURNING id", [album_data['title']]) + album_data['id'] = result.entries.first['id'] + album_data + end + end + + def self.destroy(db, album_id) + db.exec("DELETE FROM album_genres WHERE album_id = $1", [album_id]) + db.exec("DELETE FROM songs WHERE album_id = $1", [album_id]) + db.exec("DELETE FROM albums WHERE id = $1", [album_id]) + end + + end +end diff --git a/lib/songify/genre_repo.rb b/lib/songify/genre_repo.rb new file mode 100644 index 00000000..a6ea3781 --- /dev/null +++ b/lib/songify/genre_repo.rb @@ -0,0 +1,42 @@ +module Songify + class GenreRepo + def self.albums(db, genre_id) + db.exec("SELECT g.name AS genre, a.title AS title, a.id AS album_id, g.id AS genre_id FROM genres g JOIN album_genres j ON g.id = j.genre_id JOIN albums a ON a.id = j.album_id WHERE g.id = $1", [genre_id]) + end + + def self.all(db) + # Other code should not have to deal with the PG:Result. + # Therefore, convert the results into a plain array. + db.exec("SELECT * FROM genres").to_a + end + + def self.find(db, genre_id) + db.exec("SELECT * FROM genres WHERE id=$1", [genre_id]).first + end + + def self.genres_by_album(db, album_id) + db.exec("SELECT g.name, g.id FROM album_genres a JOIN genres g ON g.id = a.genre_id WHERE a.album_id = $1", [album_id]).to_a + end + + def self.save(db, genre_data) + if genre_data['id'] + result = db.exec("UPDATE genres SET name = $2 WHERE id = $1", [genre_data['id'], genre_data['name']]) + self.find(db, genre_data['id']) + else + raise "name is required." if genre_data['name'].nil? || genre_data['name'] == '' + + result = db.exec("INSERT INTO genres (name) VALUES ($1) RETURNING id", [genre_data['name']]) + genre_data['id'] = result.entries.first['id'] + genre_data + end + end + + def self.destroy(db, genre_id) + # TODO: Delete SQL statement + # ALSO DELETE JOIN TABLE ENTRIES BETWEEN THIS GENRE AND ITS ALBUMS + db.exec("DELETE FROM album_genres WHERE genre_id = $1", [genre_id]) + db.exec("DELETE FROM genres WHERE id = $1", [genre_id]) + end + + end +end diff --git a/lib/songify/song_repo.rb b/lib/songify/song_repo.rb new file mode 100644 index 00000000..f9c16e50 --- /dev/null +++ b/lib/songify/song_repo.rb @@ -0,0 +1,36 @@ +module Songify + class SongRepo + + def self.all(db) + # Other code should not have to deal with the PG:Result. + # Therefore, convert the results into a plain array. + db.exec("SELECT * FROM songs").to_a + end + + def self.find(db, song_id) + db.exec("SELECT * FROM songs WHERE id=$1", [song_id]).first + end + + def self.list(db, album_id) + db.exec("SELECT a.title AS title, s.title AS name, a.id AS album_id FROM songs s JOIN albums a ON a.id = s.album_id WHERE album_id = $1", [album_id]).to_a + end + + def self.save(db, song_data) + if song_data['id'] + result = db.exec("UPDATE songs SET title = $2 WHERE id = $1", [song_data['id'], song_data['title']]) + self.find(db, song_data['id']) + else + raise "title is required." if song_data['title'].nil? || song_data['title'] == '' + + # Ensure album exists + album = AlbumRepo.find(db, song_data['album_id']) + raise "A valid album_id is required." if album.nil? + + result = db.exec("INSERT INTO songs (title, album_id) VALUES ($1, $2) RETURNING id", [song_data['title'], song_data['album_id']]) + song_data['id'] = result.entries.first['id'] + song_data + end + end + + end +end diff --git a/server.rb b/server.rb new file mode 100644 index 00000000..18161012 --- /dev/null +++ b/server.rb @@ -0,0 +1,101 @@ +require 'sinatra' +require './lib/songify.rb' +# Rack::MethodOverride + +set :bind, '0.0.0.0' # This is needed for Vagrant + +get '/' do + erb :index +end + +get '/albums' do + @db = Songify.create_db_connection('songify_dev') + @albums = Songify::AlbumRepo.all(@db) + @genres = Songify::GenreRepo.all(@db) + erb :"albums/index" +end + +post '/albums' do + db = Songify.create_db_connection('songify_dev') + album = Songify::AlbumRepo.save(db, { + 'title' => params[:title], + 'genre_ids' => params[:genre_ids] + }) + redirect to '/albums' +end + +get '/albums/:id' do + db = Songify.create_db_connection('songify_dev') + @albums = Songify::AlbumRepo.find(db, params[:id]) + @songs = Songify::SongRepo.list(db, params[:id]) + @genres_join = Songify::GenreRepo.genres_by_album(db, params[:id]) + erb :"albums/album" +end + +delete '/albums/:id' do + db = Songify.create_db_connection('songify_dev') + Songify::AlbumRepo.destroy(db, params[:album_id]) + redirect to '/albums' +end + +get '/albums/:id/edit' do + db = Songify.create_db_connection('songify_dev') + @albums = Songify::AlbumRepo.find(db, params[:id]) + @genres_join = Songify::GenreRepo.genres_by_album(db, params[:id]) + @genres = Songify::GenreRepo.all(db) + erb :"albums/edit" +end + +post '/albums/:id/edit' do + db = Songify.create_db_connection('songify_dev') + puts params[:id].class + puts params[:genre_ids].class + Songify::AlbumRepo.add_genres(db, { + 'id' => params[:id], + 'genre_ids' => params[:genre_ids] + }) + redirect to "/albums/#{params[:id]}/edit" +end + +get '/songs' do + db = Songify.create_db_connection('songify_dev') + @songs = Songify::SongRepo.all(db) + @albums = Songify::AlbumRepo.all(db) + erb :"songs/index" +end + +post '/songs' do + db = Songify.create_db_connection('songify_dev') + song = Songify::SongRepo.save(db, { + 'title' => params[:title], + 'album_id' => params[:album_id] + }) + redirect to '/songs' +end + +get '/genres' do + db = Songify.create_db_connection('songify_dev') + @genres = Songify::GenreRepo.all(db) + erb :"genres/index" +end + +post '/genres' do + db = Songify.create_db_connection('songify_dev') + album = Songify::GenreRepo.save(db, { + 'name' => params[:name] + }) + redirect to '/genres' +end + +get '/genres/:id' do + db = Songify.create_db_connection('songify_dev') + @genres = Songify::GenreRepo.find(db, params[:id]) + @genres_join = Songify::GenreRepo.albums(db, params[:id]) + erb :"genres/genre" +end + +delete '/genres/:id' do + db = Songify.create_db_connection('songify_dev') + Songify::GenreRepo.destroy(db, params[:genre_id]) + redirect to '/genres' +end diff --git a/spec/.gitkeep b/spec/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/spec/repos/album_repo_spec.rb b/spec/repos/album_repo_spec.rb new file mode 100644 index 00000000..931ebde1 --- /dev/null +++ b/spec/repos/album_repo_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe Songify::AlbumRepo do + + def album_count + repo.all(db).count + end + + let(:repo) { Songify::AlbumRepo } + let(:db) { Songify.create_db_connection('songify_test') } + + before(:each) do + Songify.clear_db(db) + end + + it "gets all albums" do + album = repo.save(db, { 'title' => "Allybum" }) + album = repo.save(db, { 'title' => "Bluesbum" }) + + albums = repo.all(db) + expect(albums).to be_a Array + expect(albums.count).to eq 2 + + titles = albums.map {|u| u['title'] } + expect(titles).to include "Allybum", "Bluesbum" + end + + it "creates albums" do + expect(album_count).to eq 0 + + album = repo.save(db, { 'title' => "Allybum" }) + expect(album['id']).to_not be_nil + expect(album['title']).to eq "Allybum" + + # Check for persistence + expect(album_count).to eq 1 + + album = repo.all(db).first + expect(album['title']).to eq "Allybum" + end + + it "requires a title" do + expect { repo.save(db, {}) }.to raise_error {|e| + expect(e.message).to match /title/ + } + end + + it "can be assigned genres" do + gid_1 = Songify::GenreRepo.save(db, { 'name' => 'rock' }) + gid_2 = Songify::GenreRepo.save(db, { 'name' => 'avant-garde' }) + gid_3 = Songify::GenreRepo.save(db, { 'name' => 'jazz' }) + + album = repo.save(db, { 'title' => 'Suspicious Activity?', + 'genre_ids' => [gid_1['id'], gid_2['id'], gid_3['id']] }) + album = repo.find(db, album['id']) + expect(album['genres'].count).to eq 3 + + names = album['genres'].map {|g| g['name'] } + expect(names).to include 'rock', 'avant-garde', 'jazz' + end + + it "finds albums" do + album = repo.save(db, { 'title' => "Allybum" }) + retrieved_song = repo.find(db, album['id']) + expect(retrieved_song['title']).to eq "Allybum" + end + + it "updates albums" do + song1 = repo.save(db, { 'title' => "Allybum" }) + song2 = repo.save(db, { 'id' => song1['id'], 'title' => "Alicia" }) + expect(song2['id']).to eq(song1['id']) + expect(song2['title']).to eq "Alicia" + + # Check for persistence + song3 = repo.find(db, song1['id']) + expect(song3['title']).to eq "Alicia" + end + +end diff --git a/spec/repos/genre_repo_spec.rb b/spec/repos/genre_repo_spec.rb new file mode 100644 index 00000000..3e7664bf --- /dev/null +++ b/spec/repos/genre_repo_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Songify::GenreRepo do + + def genre_count + repo.all(db).count + end + + let(:repo) { Songify::GenreRepo } + let(:db) { Songify.create_db_connection('songify_test') } + + before(:each) do + Songify.clear_db(db) + end + + it "gets all genres" do + genre = repo.save(db, { 'name' => "The Ally" }) + genre = repo.save(db, { 'name' => "Barnway Blues" }) + + genres = repo.all(db) + expect(genres).to be_a Array + expect(genres.count).to eq 2 + + titles = genres.map {|u| u['name'] } + expect(titles).to include "The Ally", "Barnway Blues" + end + + it "creates genres" do + expect(genre_count).to eq 0 + + genre = repo.save(db, { 'name' => "The Ally" }) + expect(genre['id']).to_not be_nil + expect(genre['name']).to eq "The Ally" + + # Check for persistence + expect(genre_count).to eq 1 + + genre = repo.all(db).first + expect(genre['name']).to eq "The Ally" + end + + it "requires a name" do + expect { repo.save(db, {}) }.to raise_error {|e| + expect(e.message).to match /name/ + } + end + + + it "finds genres" do + genre = repo.save(db, { 'name' => "The Ally" }) + retrieved_song = repo.find(db, genre['id']) + expect(retrieved_song['name']).to eq "The Ally" + end + + it "updates genres" do + song1 = repo.save(db, { 'name' => "The Ally" }) + song2 = repo.save(db, { 'id' => song1['id'], 'name' => "Alicia" }) + expect(song2['id']).to eq(song1['id']) + expect(song2['name']).to eq "Alicia" + + # Check for persistence + song3 = repo.find(db, song1['id']) + expect(song3['name']).to eq "Alicia" + end + +end diff --git a/spec/repos/song_repo_spec.rb b/spec/repos/song_repo_spec.rb new file mode 100644 index 00000000..d9a823e7 --- /dev/null +++ b/spec/repos/song_repo_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' + +describe Songify::SongRepo do + + def song_count + repo.all(db).count + end + + let(:repo) { Songify::SongRepo } + let(:db) { Songify.create_db_connection('songify_test') } + + before(:each) do + Songify.clear_db(db) + @album_id = Songify::AlbumRepo.save(db, { 'title' => "MegaCorps" })['id'] + end + + it "gets all songs" do + song = repo.save(db, { 'album_id' => @album_id, 'title' => "The Ally" }) + song = repo.save(db, { 'album_id' => @album_id, 'title' => "Barnway Blues" }) + + songs = repo.all(db) + expect(songs).to be_a Array + expect(songs.count).to eq 2 + + titles = songs.map {|u| u['title'] } + expect(titles).to include "The Ally", "Barnway Blues" + end + + it "creates songs" do + expect(song_count).to eq 0 + + song = repo.save(db, { 'album_id' => @album_id, 'title' => "The Ally" }) + expect(song['id']).to_not be_nil + expect(song['title']).to eq "The Ally" + + # Check for persistence + expect(song_count).to eq 1 + + song = repo.all(db).first + expect(song['title']).to eq "The Ally" + end + + it "requires a title" do + expect { repo.save(db, {}) }.to raise_error {|e| + expect(e.message).to match /title/ + } + end + + it "requires an album id" do + expect { + repo.save(db, { 'title' => "The Ally" }) + } + .to raise_error {|e| + expect(e.message).to match /album_id/ + } + end + + it "requires an album id that exists" do + expect { + repo.save(db, { 'album_id' => 999, 'title' => "The Ally" }) + } + .to raise_error {|e| + expect(e.message).to match /album_id/ + } + end + + it "finds songs" do + song = repo.save(db, { 'album_id' => @album_id, 'title' => "The Ally" }) + retrieved_song = repo.find(db, song['id']) + expect(retrieved_song['title']).to eq "The Ally" + end + + it "updates songs" do + song1 = repo.save(db, { 'album_id' => @album_id, 'title' => "The Ally" }) + song2 = repo.save(db, { 'id' => song1['id'], 'title' => "Alicia" }) + expect(song2['id']).to eq(song1['id']) + expect(song2['title']).to eq "Alicia" + + # Check for persistence + song3 = repo.find(db, song1['id']) + expect(song3['title']).to eq "Alicia" + end + +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..b15d3bbc --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,13 @@ +require 'songify' + +RSpec.configure do |config| + config.treat_symbols_as_metadata_keys_with_true_values = true + config.run_all_when_everything_filtered = true + config.filter_run :focus + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = 'random' +end diff --git a/views/albums/album.erb b/views/albums/album.erb new file mode 100644 index 00000000..99050923 --- /dev/null +++ b/views/albums/album.erb @@ -0,0 +1,33 @@ +<- Back to Everything +

<%= @albums['title'] %> - All Songs

+

+<% @genres_join.each do |genre| %> + <%= genre['name'] %> | +<% end %> +edit +

+ + +
+ + + +
+ + + \ No newline at end of file diff --git a/views/albums/edit.erb b/views/albums/edit.erb new file mode 100644 index 00000000..e4643920 --- /dev/null +++ b/views/albums/edit.erb @@ -0,0 +1,39 @@ +<- Back to Everything +

<%= @albums['title'] %> - Edit Genres

+

+<% @genres_join.each do |genre| %> + <%= genre['name'] %> | +<% end %> +

+ +
+ +
+ + add genre +
+ +
+ + + \ No newline at end of file diff --git a/views/albums/index.erb b/views/albums/index.erb new file mode 100644 index 00000000..25dc7567 --- /dev/null +++ b/views/albums/index.erb @@ -0,0 +1,42 @@ +<- Back to Everything +

All Albums

+ + + +
+

New Album

+ + +
+ + add genre +
+ +
+ + + diff --git a/views/genres/genre.erb b/views/genres/genre.erb new file mode 100644 index 00000000..f26d7691 --- /dev/null +++ b/views/genres/genre.erb @@ -0,0 +1,28 @@ +<- Back to Everything +

<%= @genres['name'] %> - All Albums

+ + + +
+ + + +
+ + + \ No newline at end of file diff --git a/views/genres/index.erb b/views/genres/index.erb new file mode 100644 index 00000000..bc8c6edf --- /dev/null +++ b/views/genres/index.erb @@ -0,0 +1,15 @@ +<- Back to Everything +

All Genres

+ + + +
+

New Genre

+ + + +
diff --git a/views/index.erb b/views/index.erb new file mode 100644 index 00000000..10abf7f6 --- /dev/null +++ b/views/index.erb @@ -0,0 +1,9 @@ +

The Songify System

+ +

You're gonna be big

+ + diff --git a/views/songs/index.erb b/views/songs/index.erb new file mode 100644 index 00000000..a4e60abf --- /dev/null +++ b/views/songs/index.erb @@ -0,0 +1,21 @@ +<- Back to Everything +

All Songs

+ + + +
+

New Song

+ + + + + +