diff --git a/.rubocop.yml b/.rubocop.yml index fa6eab6..c575763 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -37,4 +37,7 @@ Layout/TrailingWhitespace: # require exactly one final newline at end of each file Layout/TrailingEmptyLines: - Enabled: true \ No newline at end of file + Enabled: true + +RSpec/MultipleExpectations: + Max: 2 \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a26e8b4..56476a3 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -14,7 +14,7 @@ def logged_in? def require_admin unless current_user&.role == "admin" flash[:alert] = "You do not have permission to do that." - redirect_to root_path + redirect_to dashboard_path end end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 91905ae..863cf28 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,6 +1,17 @@ class DashboardController < ApplicationController + before_action :require_login + def index @recent_tracks = Track.order(created_at: :desc).limit(5) @recent_comments = Comment.order(created_at: :desc).limit(5) end + + private + + def require_login + unless logged_in? + flash[:alert] = "Please log in to access the dashboard" + redirect_to login_path + end + end end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 0000000..95f2992 --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,4 @@ +class HomeController < ApplicationController + def index + end +end diff --git a/app/controllers/samples_controller.rb b/app/controllers/samples_controller.rb new file mode 100644 index 0000000..2954d0d --- /dev/null +++ b/app/controllers/samples_controller.rb @@ -0,0 +1,24 @@ +class SamplesController < ApplicationController + before_action :require_admin + + def new + @sample = Sample.new + @tracks = Track.order(:title) + end + + def create + @sample = Sample.new(sample_params) + if @sample.save + redirect_to dashboard_path, notice: "Sample Connection created!" + else + @tracks = Track.order(:title) + render :new, status: :unprocessable_entity + end + end + + private + + def sample_params + params.require(:sample).permit(:derived_track_id, :source_track_id) + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 095eeb1..c7341f0 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -5,10 +5,11 @@ def new def create user = User.find_by(username: params[:username]) + if user&.authenticate(params[:password]) session[:user_id] = user.id session[:role] = user.role - redirect_to root_path, notice: "Logged in as #{user.username}" + redirect_to dashboard_path, notice: "Logged in as #{user.username}" else flash.now[:alert] = "Invalid username or password" render :new, status: :unprocessable_entity diff --git a/app/controllers/tracks_controller.rb b/app/controllers/tracks_controller.rb index 8e6eeb7..6268b16 100644 --- a/app/controllers/tracks_controller.rb +++ b/app/controllers/tracks_controller.rb @@ -18,24 +18,28 @@ def new def create @track = Track.new(track_params) - @track.user = User.find_by(username: "Twhite") + @track.user = current_user + if @track.save redirect_to @track, notice: "Track Created!" else + @artists = Artist.alphabetical render :new, status: :unprocessable_entity end end def edit @track = Track.find(params[:id]) + @artists = Artist.alphabetical end def update @track = Track.find(params[:id]) - @track.user = User.find_by(username: "Twhite") + @track.user = current_user if @track.update(track_params) redirect_to @track, notice: "Track Updated" else + @artists = Artist.alphabetical render :edit, status: :unprocessable_entity end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 0000000..7f25bb4 --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,22 @@ +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 dashboard_path, notice: "Welcome, #{@user.username}!" + else + flash.now[:alert] = @user.errors.full_messages.to_sentence + render :new, status: :unprocessable_entity + end + end + + private + + def user_params + params.require(:user).permit(:username, :email, :password, :password_confirmation) + end +end diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb new file mode 100644 index 0000000..23de56a --- /dev/null +++ b/app/helpers/home_helper.rb @@ -0,0 +1,2 @@ +module HomeHelper +end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb new file mode 100644 index 0000000..2310a24 --- /dev/null +++ b/app/helpers/users_helper.rb @@ -0,0 +1,2 @@ +module UsersHelper +end diff --git a/app/models/artist.rb b/app/models/artist.rb index 563c36b..cce7283 100644 --- a/app/models/artist.rb +++ b/app/models/artist.rb @@ -1,7 +1,7 @@ class Artist < ApplicationRecord has_many :tracks, dependent: :destroy - validates :name, presence: true + validates :name, presence: true, uniqueness: true # alpha scope scope :alphabetical, -> { order("LOWER(name) ASC") } diff --git a/app/models/sample.rb b/app/models/sample.rb index f02e6b7..b3d350b 100644 --- a/app/models/sample.rb +++ b/app/models/sample.rb @@ -4,6 +4,27 @@ class Sample < ApplicationRecord has_many :sample_segments, dependent: :destroy - # TODO Validate later sampler_entry_id can not be equal sampled_entry_id - # AKA a song can not sample itself or be sampled by itself + validates :derived_track_id, presence: true + validates :source_track_id, presence: true + validate :no_self_sampling + validate :no_sampling_from_future + + # a source id cannot be equal to a derived id on sample join table + def no_self_sampling + if derived_track_id == source_track_id + errors.add(:base, "A track cannot sample itself") + end + end + + # check sourceYear exist, derivedYear exist, then if derived is less than source throw error + def no_sampling_from_future + if source_track&.year && derived_track&.year && derived_track&.year < source_track&.year + errors.add(:base, "A track cannot sample from the future") + end + end + + # behavior methods + def description + "#{derived_track.title} samples #{source_track.title}" + end end diff --git a/app/models/track.rb b/app/models/track.rb index 07954bc..41c8099 100644 --- a/app/models/track.rb +++ b/app/models/track.rb @@ -27,6 +27,20 @@ class Track < ApplicationRecord source: :derived_track validates :title, presence: true - validates :year, presence: true + validates :year, numericality: { greater_than_or_equal_to: 1900, less_than_or_equal_to: Date.current.year } validates :bpm, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true + + # behavior methods + + def formatted_title + "#{title} (#{year})" +end + + def sample_count + samples_used.count + end + + def sampled_by_count + sampled_by_tracks.count +end end diff --git a/app/models/user.rb b/app/models/user.rb index ac6aa84..f1bac00 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,10 +1,16 @@ class User < ApplicationRecord has_many :tracks - has_many :comments - # TODO gameplan user.delete, what happens to their entries and comments. + # future nullify user id on orphaned tracks? + has_many :comments, dependent: :destroy has_secure_password + after_initialize :set_default_role, if: :new_record? + + def set_default_role + self.role ||= "user" + end + validates :email, presence: true, uniqueness: true validates :username, presence: true, uniqueness: true diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb index 7257866..352fd65 100644 --- a/app/views/dashboard/index.html.erb +++ b/app/views/dashboard/index.html.erb @@ -1,4 +1,9 @@ -

Dashboard

+ +<% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+<% end %>

Recent Tracks

-

- <%= link_to "Add New Track", new_track_path %> -

\ No newline at end of file +
+ <%= link_to "Add New Track", new_track_path, + style: "padding: 10px 20px; background-color: #007bff; color: white; text-decoration: none; border-radius: 5px; margin-right: 10px;" %> + + <%= link_to "Add New Sample Connection", new_sample_path, + style: "padding: 10px 20px; background-color: #28a745; color: white; text-decoration: none; border-radius: 5px; margin-right: 10px;" %> + + <%= link_to "View All Tracks", tracks_path, + style: "padding: 10px 20px; background-color: #6c757d; color: white; text-decoration: none; border-radius: 5px;" %> +
\ No newline at end of file diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb new file mode 100644 index 0000000..e48b7e2 --- /dev/null +++ b/app/views/home/index.html.erb @@ -0,0 +1,16 @@ +
+

Sample DB

+

A database of who sampled who.

+
+ + <%= link_to "View All Tracks", tracks_path, class: "btn", + style: "padding: 10px 20px; background-color: #6c757d; color: white; text-decoration: none; border-radius: 5px; margin-right: 10px;" %> + + <%= link_to "Log In", login_path, class: "btn", + style: "padding: 10px 20px; background-color: #007bff; color: white; text-decoration: none; border-radius: 5px; margin-right: 10px;" %> + + <%= link_to "Sign Up", signup_path, class: "btn", + style: "padding: 10px 20px; background-color: #28a745; color: white; text-decoration: none; border-radius: 5px;" %> + +
+
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index d5d1cd2..69ae419 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -22,22 +22,44 @@ <%= javascript_importmap_tags %> - - <% if flash[:notice] %> -

<%= flash[:notice] %>

- <% end %> - - <% if flash[:alert] %> -

<%= flash[:alert] %>

- <% end %> - - <% if logged_in? %> -

Logged in as <%= current_user.username %>

- <%= button_to "Logout", logout_path, method: :delete %> - <% else %> - <%= link_to "Login", login_path %> - <% end %> - - <%= yield %> - - + +
+
+ +
+ <% if logged_in? %> + <%= button_to "Logout", logout_path, method: :delete %> + <% else %> + <%= link_to "Login", login_path %> + <% end %> +
+ + +
+ <% if logged_in? %> +

Logged in as <%= current_user.username %>

+ <% end %> +
+ + +
+ <%= link_to "Dashboard", dashboard_path %> +
+
+
+ + <%= yield %> + + <% if flash[:notice] %> +
+ <%= flash[:notice] %> +
+ <% end %> + + <% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+ <% end %> + + \ No newline at end of file diff --git a/app/views/samples/new.html.erb b/app/views/samples/new.html.erb new file mode 100644 index 0000000..d5e9737 --- /dev/null +++ b/app/views/samples/new.html.erb @@ -0,0 +1,27 @@ +

Add Sample Connection

+ +<%= form_with model: @sample, local: true do |f| %> + <% if @sample.errors.any? %> +
+ +
+ <% end %> + +
+
+ <%= f.label :derived_track_id, "New Track" %>
+ <%= f.collection_select :derived_track_id, @tracks, :id, :title, prompt: "Select sampler" %> +
+ +
+ <%= f.label :source_track_id, "Original Source" %>
+ <%= f.collection_select :source_track_id, @tracks, :id, :title, prompt: "Select sampled track" %> +
+
+ + <%= f.submit "Create Sample Connection" %> +<% end %> \ No newline at end of file diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index 0fa8185..fe59994 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -1,17 +1,24 @@ -

Login

+
+

Log In

-<%= form_with url: login_path, local: true do |form| %> -
- <%= form.label :username %>
- <%= form.text_field :username %> -
- -
- <%= form.label :password %>
- <%= form.password_field :password %> -
+ <%= form_with url: login_path, local: true do |form| %> -
- <%= form.submit "Login" %> + <% if flash[:alert] %> +
+

<%= flash[:alert] %>

<% end %> + +
+ <%= form.label :username %>
+ <%= form.text_field :username %> +
+ +
+ <%= form.label :password %>
+ <%= form.password_field :password %> +
+ + <%= form.submit "Log In", class: "btn btn-primary" %> + <% end %> +
diff --git a/app/views/tracks/show.html.erb b/app/views/tracks/show.html.erb index 0b15a5d..15a77a5 100644 --- a/app/views/tracks/show.html.erb +++ b/app/views/tracks/show.html.erb @@ -1,45 +1,68 @@ -

Track: <%= @track.title %>

+

<%= @track.formatted_title %>

+

- Artist: <%= @track.artist.name %>
- Year: <%= @track.year %>
- BPM: <%= @track.bpm %> + Artist: <%= link_to @track.artist.name, artist_path(@track.artist) %>
+ Added by: <%= @track.user.username %>
+ BPM: <%= @track.bpm || "N/A" %>
+ KEY: <%= @track.key || "N/A" %>
+ Samples used: <%= @track.sample_count %>
+ Sampled by others: <%= @track.sampled_by_count %> +

-

Samples Used

+
+

Samples Used

<% if @track.sampled_tracks.any? %> <% else %>

None

-<% end %> +<% end %> -

Sampled By

+
+

Sampled By

<% if @track.sampled_by_tracks.any? %> <% else %>

None

-<% end %> +<% end %> + +

Comments

- +<% if @track.comments.any? %> + +<% else %> +

No comments yet.

+<% end %> -

- <%= link_to "Edit Track", edit_track_path(@track) %> | - <%= link_to "Delete Track", track_path(@track), - data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete this track?" } %> -

\ No newline at end of file +
+ +<% if logged_in? %> +

+ <%= link_to "Edit Track", edit_track_path(@track) %> | + <% if current_user.role == "admin" %> + <%= link_to "Delete Track", track_path(@track), + data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %> + <% end %> +

+<% end %> \ No newline at end of file diff --git a/app/views/users/create.html.erb b/app/views/users/create.html.erb new file mode 100644 index 0000000..48ea02e --- /dev/null +++ b/app/views/users/create.html.erb @@ -0,0 +1,2 @@ +

Users#create

+

Find me in app/views/users/create.html.erb

diff --git a/app/views/users/new.html.erb b/app/views/users/new.html.erb new file mode 100644 index 0000000..4fce5fa --- /dev/null +++ b/app/views/users/new.html.erb @@ -0,0 +1,38 @@ +
+

Sign Up

+ +<% if @user.errors.any? %> +
+

<%= pluralize(@user.errors.count, "error") %> prevented sign up:

+
    + <% @user.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+<% end %> + + <%= form_with model: @user, url: signup_path, local: true do |form| %> +
+ <%= form.label :username %>
+ <%= form.text_field :username %> +
+ +
+ <%= form.label :email %>
+ <%= form.email_field :email %> +
+ +
+ <%= form.label :password %>
+ <%= form.password_field :password %> +
+ +
+ <%= form.label :password_confirmation %>
+ <%= form.password_field :password_confirmation %> +
+ + <%= form.submit "Create Account", class: "btn btn-primary" %> + <% end %> +
\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 06d2d06..996ef18 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,9 +1,14 @@ Rails.application.routes.draw do + get "users/new" + get "users/create" + get "home/index" resources :artists, only: [ :index, :show, :new, :create ] resources :tracks, only: [ :index, :show, :new, :create, :edit, :update, :destroy ] + resources :samples, only: [ :new, :create ] get "/dashboard", to: "dashboard#index" - + get "signup", to: "users#new" + post "signup", to: "users#create" # session routes get "login", to: "sessions#new" post "login", to: "sessions#create" @@ -13,5 +18,5 @@ get "up" => "rails/health#show", as: :rails_health_check # ("/") makes root dashboard - root "dashboard#index" + root "home#index" end diff --git a/db/migrate/20251021021206_add_null_constraint_to_password_digest.rb b/db/migrate/20251021021206_add_null_constraint_to_password_digest.rb new file mode 100644 index 0000000..bc373b2 --- /dev/null +++ b/db/migrate/20251021021206_add_null_constraint_to_password_digest.rb @@ -0,0 +1,5 @@ +class AddNullConstraintToPasswordDigest < ActiveRecord::Migration[8.0] + def change + change_column_null :users, :password_digest, false + end +end diff --git a/db/migrate/20251021021407_add_unique_index_to_artists_name.rb b/db/migrate/20251021021407_add_unique_index_to_artists_name.rb new file mode 100644 index 0000000..ae3f035 --- /dev/null +++ b/db/migrate/20251021021407_add_unique_index_to_artists_name.rb @@ -0,0 +1,6 @@ +class AddUniqueIndexToArtistsName < ActiveRecord::Migration[8.0] + def change + remove_index :artists, :name if index_exists?(:artists, :name) + add_index :artists, :name, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 6b9d551..e7acd3a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,12 +10,12 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_10_07_214251) do +ActiveRecord::Schema[8.0].define(version: 2025_10_21_021407) do create_table "artists", force: :cascade do |t| t.string "name", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index ["name"], name: "index_artists_on_name" + t.index ["name"], name: "index_artists_on_name", unique: true end create_table "comments", force: :cascade do |t| @@ -67,7 +67,7 @@ t.string "username", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.string "password_digest" + t.string "password_digest", null: false t.string "role", default: "user", null: false t.index ["email"], name: "index_users_on_email", unique: true t.index ["username"], name: "index_users_on_username", unique: true diff --git a/spec/helpers/home_helper_spec.rb b/spec/helpers/home_helper_spec.rb new file mode 100644 index 0000000..a54e1a1 --- /dev/null +++ b/spec/helpers/home_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the HomeHelper. For example: +# +# describe HomeHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe HomeHelper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb new file mode 100644 index 0000000..c55ad65 --- /dev/null +++ b/spec/helpers/users_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the UsersHelper. For example: +# +# describe UsersHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe UsersHelper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/sample_spec.rb b/spec/models/sample_spec.rb new file mode 100644 index 0000000..da996d1 --- /dev/null +++ b/spec/models/sample_spec.rb @@ -0,0 +1,30 @@ +require "rails_helper" + +RSpec.describe Sample do + let(:artist) { Artist.create!(name: "Test Artist") } + let(:user) { User.create!(username: "tester", email: "t@mail.com", password: "password") } + let(:source) { Track.create!(title: "Original", year: 1990, artist: artist, user: user) } + let(:derived) { Track.create!(title: "Remix", year: 1995, artist: artist, user: user) } + + it "is valid with valid attributes" do + sample = described_class.new(source_track: source, derived_track: derived) + expect(sample).to be_valid + end + + it "is invalid when a track samples itself" do + sample = described_class.new(source_track: source, derived_track: source) + expect(sample).not_to be_valid + expect(sample.errors[:base]).to include("A track cannot sample itself") + end + + it "is invalid when sampling from the future" do + sample = described_class.new(source_track: derived, derived_track: source) + expect(sample).not_to be_valid + expect(sample.errors[:base]).to include("A track cannot sample from the future") + end + + it "returns a readable description" do + sample = described_class.new(source_track: source, derived_track: derived) + expect(sample.description).to eq("Remix samples Original") + end +end diff --git a/spec/requests/dashboard_spec.rb b/spec/requests/dashboard_spec.rb new file mode 100644 index 0000000..0fcf53a --- /dev/null +++ b/spec/requests/dashboard_spec.rb @@ -0,0 +1,9 @@ +require "rails_helper" + +RSpec.describe "Dashboard" do + it "renders succesfully" do + get "/dashboard" + expect(response).to have_http_status(:ok) + expect(response.body).to include("Track").or include("Dashboard") + end +end diff --git a/spec/requests/home_spec.rb b/spec/requests/home_spec.rb new file mode 100644 index 0000000..3d279d2 --- /dev/null +++ b/spec/requests/home_spec.rb @@ -0,0 +1,10 @@ +require 'rails_helper' + +RSpec.describe "Homes" do + describe "GET /index" do + it "returns http success" do + get "/home/index" + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/requests/users_spec.rb b/spec/requests/users_spec.rb new file mode 100644 index 0000000..2ecc182 --- /dev/null +++ b/spec/requests/users_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +RSpec.describe "Users" do + describe "GET /new" do + it "returns http success" do + get "/users/new" + expect(response).to have_http_status(:success) + end + end + + describe "GET /create" do + it "returns http success" do + get "/users/create" + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/views/home/index.html.erb_spec.rb b/spec/views/home/index.html.erb_spec.rb new file mode 100644 index 0000000..ea77ece --- /dev/null +++ b/spec/views/home/index.html.erb_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe "home/index.html.erb" do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/views/users/create.html.erb_spec.rb b/spec/views/users/create.html.erb_spec.rb new file mode 100644 index 0000000..738fd62 --- /dev/null +++ b/spec/views/users/create.html.erb_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe "users/create.html.erb" do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/views/users/new.html.erb_spec.rb b/spec/views/users/new.html.erb_spec.rb new file mode 100644 index 0000000..96b161f --- /dev/null +++ b/spec/views/users/new.html.erb_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe "users/new.html.erb" do + pending "add some examples to (or delete) #{__FILE__}" +end