diff --git a/db/manifest b/db/manifest index 3625cb735..13d9784e6 100755 --- a/db/manifest +++ b/db/manifest @@ -226,3 +226,6 @@ add_session_create_type.sql user_syncs_and_quick_mix.sql user_syncs_fix_dup_tracks_2408.sql deletable_recordings.sql +jam_tracks.sql +shopping_carts.sql +recurly.sql \ No newline at end of file diff --git a/db/up/jam_tracks.sql b/db/up/jam_tracks.sql new file mode 100644 index 000000000..a6e716fa2 --- /dev/null +++ b/db/up/jam_tracks.sql @@ -0,0 +1,68 @@ +CREATE TABLE jam_track_licensors ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + name VARCHAR NOT NULL UNIQUE, + description TEXT, + attention TEXT, + address_line_1 VARCHAR, + address_line_2 VARCHAR, + city VARCHAR, + state VARCHAR, + zip_code VARCHAR, + contact VARCHAR, + email VARCHAR, + phone VARCHAR, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL +); + +CREATE TABLE jam_tracks ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + name VARCHAR NOT NULL UNIQUE, + description TEXT, + bpm decimal, + time_signature VARCHAR, + status VARCHAR, + recording_type VARCHAR, + original_artist TEXT, + songwriter TEXT, + publisher TEXT, + pro VARCHAR, + sales_region VARCHAR, + price decimal, + reproduction_royalty BOOLEAN, + public_performance_royalty BOOLEAN, + reproduction_royalty_amount DECIMAL, + licensor_royalty_amount DECIMAL, + pro_royalty_amount DECIMAL, + url VARCHAR, + md5 VARCHAR, + length BIGINT, + licensor_id VARCHAR(64) REFERENCES jam_track_licensors (id) ON DELETE SET NULL, + genre_id VARCHAR(64) REFERENCES genres (id) ON DELETE SET NULL, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL +); + +CREATE TABLE jam_track_tracks ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + position INTEGER, + track_type VARCHAR, + jam_track_id VARCHAR(64) REFERENCES jam_tracks(id) ON DELETE CASCADE, + instrument_id VARCHAR(64) REFERENCES instruments(id) ON DELETE SET NULL, + part VARCHAR, + url VARCHAR, + md5 VARCHAR, + length BIGINT, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL +); + +CREATE INDEX jam_track_tracks_position_uniqkey ON jam_track_tracks (position, jam_track_id); + +CREATE TABLE jam_track_rights ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + user_id VARCHAR(64) NOT NULL REFERENCES users(id), + jam_track_id VARCHAR(64) NOT NULL REFERENCES jam_tracks(id) +); + +CREATE INDEX jam_tracks_rights_uniqkey ON jam_track_rights (user_id, jam_track_id); \ No newline at end of file diff --git a/db/up/recurly.sql b/db/up/recurly.sql new file mode 100644 index 000000000..4745fa14d --- /dev/null +++ b/db/up/recurly.sql @@ -0,0 +1,2 @@ +ALTER TABLE users ADD COLUMN recurly_code VARCHAR(50) DEFAULT NULL; +ALTER TABLE jam_tracks ADD COLUMN plan_code VARCHAR(50) DEFAULT NULL; \ No newline at end of file diff --git a/db/up/shopping_carts.sql b/db/up/shopping_carts.sql new file mode 100644 index 000000000..1ec83122f --- /dev/null +++ b/db/up/shopping_carts.sql @@ -0,0 +1,10 @@ +CREATE TABLE shopping_carts ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + quantity INTEGER NOT NULL DEFAULT 1, + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + cart_id VARCHAR(64) NOT NULL, + cart_class_name VARCHAR(64), + cart_type VARCHAR(64), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 22f97a24f..07234042f 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -72,6 +72,8 @@ require "jam_ruby/app/uploaders/perf_data_uploader" require "jam_ruby/app/uploaders/recorded_track_uploader" require "jam_ruby/app/uploaders/mix_uploader" require "jam_ruby/app/uploaders/music_notation_uploader" +require "jam_ruby/app/uploaders/jam_track_uploader" +require "jam_ruby/app/uploaders/jam_track_track_uploader" require "jam_ruby/app/uploaders/max_mind_release_uploader" require "jam_ruby/lib/desk_multipass" require "jam_ruby/lib/ip" @@ -171,11 +173,16 @@ require "jam_ruby/models/email_batch_new_musician" require "jam_ruby/models/email_batch_progression" require "jam_ruby/models/email_batch_scheduled_sessions" require "jam_ruby/models/email_batch_set" +require "jam_ruby/models/jam_track_licensor" +require "jam_ruby/models/jam_track" +require "jam_ruby/models/jam_track_track" +require "jam_ruby/models/jam_track_right" require "jam_ruby/app/mailers/async_mailer" require "jam_ruby/app/mailers/batch_mailer" require "jam_ruby/app/mailers/progress_mailer" require "jam_ruby/models/affiliate_partner" require "jam_ruby/models/chat_message" +require "jam_ruby/models/shopping_cart" require "jam_ruby/models/generic_state" require "jam_ruby/models/score_history" require "jam_ruby/models/jam_company" diff --git a/ruby/lib/jam_ruby/app/uploaders/jam_track_track_uploader.rb b/ruby/lib/jam_ruby/app/uploaders/jam_track_track_uploader.rb new file mode 100644 index 000000000..4a4d8cc4f --- /dev/null +++ b/ruby/lib/jam_ruby/app/uploaders/jam_track_track_uploader.rb @@ -0,0 +1,28 @@ +class JamTrackTrackUploader < CarrierWave::Uploader::Base + # include CarrierWaveDirect::Uploader + include CarrierWave::MimeTypes + + process :set_content_type + + def initialize(*) + super + JamRuby::UploaderConfiguration.set_aws_private_configuration(self) + end + + # Add a white list of extensions which are allowed to be uploaded. + def extension_white_list + %w(ogg) + end + + def store_dir + nil + end + + def md5 + @md5 ||= ::Digest::MD5.file(current_path).hexdigest + end + + def filename + "#{model.store_dir}/#{model.filename}" if model.id + end +end diff --git a/ruby/lib/jam_ruby/app/uploaders/jam_track_uploader.rb b/ruby/lib/jam_ruby/app/uploaders/jam_track_uploader.rb new file mode 100644 index 000000000..784d646c4 --- /dev/null +++ b/ruby/lib/jam_ruby/app/uploaders/jam_track_uploader.rb @@ -0,0 +1,28 @@ +class JamTrackUploader < CarrierWave::Uploader::Base + # include CarrierWaveDirect::Uploader + include CarrierWave::MimeTypes + + process :set_content_type + + def initialize(*) + super + JamRuby::UploaderConfiguration.set_aws_private_configuration(self) + end + + # Add a white list of extensions which are allowed to be uploaded. + def extension_white_list + %w(jka) + end + + def store_dir + nil + end + + def md5 + @md5 ||= ::Digest::MD5.file(current_path).hexdigest + end + + def filename + "#{model.store_dir}/#{model.filename}" if model.id + end +end diff --git a/ruby/lib/jam_ruby/constants/validation_messages.rb b/ruby/lib/jam_ruby/constants/validation_messages.rb index 30544bc4a..7dc4d955a 100644 --- a/ruby/lib/jam_ruby/constants/validation_messages.rb +++ b/ruby/lib/jam_ruby/constants/validation_messages.rb @@ -40,6 +40,11 @@ module ValidationMessages EMAIL_MATCHES_CURRENT = "is same as your current email" INVALID_FPFILE = "is not valid" + # recurly + RECURLY_ERROR = "Error occurred during Recurly transaction." + RECURLY_ACCOUNT_ERROR = "You don't have Recurly account yet." + RECURLY_PARAMETER_ERROR = "You didn't input correct information for Recurly transaction." + #connection USER_OR_LATENCY_TESTER_PRESENT = "user or latency_tester must be present" SELECT_AT_LEAST_ONE = "Please select at least one track" # DO NOT CHANGE THIS TEXT MESSAGE UNLESS YOU CHANGE createSession.js.erb, which is looking for it diff --git a/ruby/lib/jam_ruby/lib/s3_manager.rb b/ruby/lib/jam_ruby/lib/s3_manager.rb index 8e31532d1..3b6a6bf77 100644 --- a/ruby/lib/jam_ruby/lib/s3_manager.rb +++ b/ruby/lib/jam_ruby/lib/s3_manager.rb @@ -88,6 +88,14 @@ module JamRuby end end + def exists?(filename) + s3_bucket.objects[filename].exists? + end + + def length(filename) + s3_bucket.objects[filename].content_length + end + private def s3_bucket diff --git a/ruby/lib/jam_ruby/models/genre.rb b/ruby/lib/jam_ruby/models/genre.rb index b8e61a270..e2e0faa1d 100644 --- a/ruby/lib/jam_ruby/models/genre.rb +++ b/ruby/lib/jam_ruby/models/genre.rb @@ -14,6 +14,8 @@ module JamRuby # genres has_and_belongs_to_many :recordings, :class_name => "JamRuby::Recording", :join_table => "recordings_genres" + # jam tracks + has_many :jam_tracks, :class_name => "JamRuby::JamTrack" def to_s description diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb new file mode 100644 index 000000000..b3f11dbb1 --- /dev/null +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -0,0 +1,106 @@ +module JamRuby + class JamTrack < ActiveRecord::Base + include JamRuby::S3ManagerMixin + + TIME_SIGNATURES = %w{4/4 3/4 2/4 6/8 5/8'} + STATUS = %w{Staging Production Retired} + RECORDING_TYPE = %w{Cover Original} + PRO = %w{ASCAP BMI SESAC} + SALES_REGION = ['United States', 'Worldwide'] + + PRODUCT_TYPE = 'JamTrack' + + mount_uploader :url, JamTrackUploader + + attr_accessible :name, :description, :bpm, :time_signature, :status, :recording_type, + :original_artist, :songwriter, :publisher, :licensor, :licensor_id, :pro, :genre, :genre_id, :sales_region, :price, + :reproduction_royalty, :public_performance_royalty, :reproduction_royalty_amount, + :licensor_royalty_amount, :pro_royalty_amount, :jam_track_tracks_attributes, as: :admin + + validates :name, presence: true, uniqueness: true, length: {maximum: 200} + validates :description, length: {maximum: 1000} + validates_format_of :bpm, with: /^\d+\.*\d{0,1}$/ + validates :time_signature, inclusion: {in: [nil] + TIME_SIGNATURES} + validates :status, inclusion: {in: [nil] + STATUS} + validates :recording_type, inclusion: {in: [nil] + RECORDING_TYPE} + validates :original_artist, length: {maximum: 200} + validates :songwriter, length: {maximum: 1000} + validates :publisher, length: {maximum: 1000} + validates :pro, inclusion: {in: [nil] + PRO} + validates :sales_region, inclusion: {in: [nil] + SALES_REGION} + validates_format_of :price, with: /^\d+\.*\d{0,2}$/ + + validates :reproduction_royalty, inclusion: {in: [nil, true, false]} + validates :public_performance_royalty, inclusion: {in: [nil, true, false]} + validates_format_of :reproduction_royalty_amount, with: /^\d+\.*\d{0,3}$/ + validates_format_of :licensor_royalty_amount, with: /^\d+\.*\d{0,3}$/ + validates_format_of :pro_royalty_amount, with: /^\d+\.*\d{0,3}$/ + + before_save :sanitize_active_admin + + belongs_to :genre, class_name: "JamRuby::Genre" + belongs_to :licensor , class_name: 'JamRuby::JamTrackLicensor', foreign_key: 'licensor_id' + + has_many :jam_track_tracks, :class_name => "JamRuby::JamTrackTrack", order: 'position ASC' + + has_many :jam_track_rights, :class_name => "JamRuby::JamTrackRight", inverse_of: 'jam_track', :foreign_key => "jam_track_id" + has_many :owners, :through => :jam_track_rights, :class_name => "JamRuby::User", :source => :user + + accepts_nested_attributes_for :jam_track_tracks, allow_destroy: true + + # create storage directory that will house this jam_track, as well as + def store_dir + "jam_tracks/#{created_at.strftime('%m-%d-%Y')}/#{id}" + end + + # create name of the file + def filename + "#{name}.jka" + end + + # creates a short-lived URL that has access to the object. + # the idea is that this is used when a user who has the rights to this tries to download this JamTrack + # we would verify their rights (can_download?), and generates a URL in response to the click so that they can download + # but the url is short lived enough so that it wouldn't be easily shared + def sign_url(expiration_time = 120) + s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/jka', :secure => false}) + end + + def can_download?(user) + owners.include?(user) + end + + def self.index user, options = {} + limit = options[:limit] + limit ||= 20 + limit = limit.to_i + + start = options[:start].presence + start = start.to_i || 0 + + query = JamTrack.joins(:jam_track_tracks) + .offset(start) + .limit(limit) + + query = query.where("jam_tracks.genre_id = '#{options[:genre]}'") unless options[:genre].blank? + query = query.where("jam_track_tracks.instrument_id = '#{options[:instrument]}'") unless options[:instrument].blank? + query = query.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank? + query = query.group("jam_tracks.id") + + if query.length == 0 + [query, nil] + elsif query.length < limit + [query, nil] + else + [query, start + limit] + end + end + + private + + def sanitize_active_admin + self.genre_id = nil if self.genre_id == '' + self.licensor_id = nil if self.licensor_id == '' + end + end +end diff --git a/ruby/lib/jam_ruby/models/jam_track_licensor.rb b/ruby/lib/jam_ruby/models/jam_track_licensor.rb new file mode 100644 index 000000000..d5ee3df75 --- /dev/null +++ b/ruby/lib/jam_ruby/models/jam_track_licensor.rb @@ -0,0 +1,21 @@ +module JamRuby + class JamTrackLicensor < ActiveRecord::Base + + attr_accessible :name, :description, :attention, :address_line_1, :address_line_2, + :city, :state, :zip_code, :contact, :email, :phone, as: :admin + + validates :name, presence: true, uniqueness: true, length: {maximum: 200} + validates :description, length: {maximum: 1000} + validates :attention, length: {maximum: 200} + validates :address_line_1, length: {maximum: 200} + validates :address_line_2, length: {maximum: 200} + validates :city, length: {maximum: 200} + validates :state, length: {maximum: 200} + validates :zip_code, length: {maximum: 200} + validates :contact, length: {maximum: 200} + validates :email, length: {maximum: 200} + validates :phone, length: {maximum: 200} + + has_many :jam_tracks, :class_name => "JamRuby::JamTrack", foreign_key: 'licensor_id' + end +end diff --git a/ruby/lib/jam_ruby/models/jam_track_right.rb b/ruby/lib/jam_ruby/models/jam_track_right.rb new file mode 100644 index 000000000..2a741a3a3 --- /dev/null +++ b/ruby/lib/jam_ruby/models/jam_track_right.rb @@ -0,0 +1,14 @@ +module JamRuby + + # describes what users have rights to which tracks + class JamTrackRight < ActiveRecord::Base + + belongs_to :user, class_name: "JamRuby::User" # the owner, or purchaser of the jam_track + belongs_to :jam_track, class_name: "JamRuby::JamTrack" + + validates :user, presence:true + validates :jam_track, presence:true + + validates_uniqueness_of :user_id, scope: :jam_track_id + end +end diff --git a/ruby/lib/jam_ruby/models/jam_track_track.rb b/ruby/lib/jam_ruby/models/jam_track_track.rb new file mode 100644 index 000000000..f9e8ebcb9 --- /dev/null +++ b/ruby/lib/jam_ruby/models/jam_track_track.rb @@ -0,0 +1,47 @@ +module JamRuby + + # describes an audio track (like the drums, or guitar) that comprises a JamTrack + class JamTrackTrack < ActiveRecord::Base + include JamRuby::S3ManagerMixin + + # there should only be one Master per JamTrack, but there can be N Track per JamTrack + TRACK_TYPE = %w{Master Track} + + mount_uploader :url, JamTrackTrackUploader + + attr_accessible :track_type, :instrument, :instrument_id, :position, :part, :url, as: :admin + + validates :position, presence: true, numericality: {only_integer: true}, length: {in: 1..1000} + validates :part, length: {maximum: 20} + validates :track_type, inclusion: {in: TRACK_TYPE } + validates_uniqueness_of :position, scope: :jam_track_id + validates_uniqueness_of :part, scope: :jam_track_id + # validates :jam_track, presence: true + + belongs_to :instrument, class_name: "JamRuby::Instrument" + belongs_to :jam_track, class_name: "JamRuby::JamTrack" + + # create storage directory that will house this jam_track, as well as + def store_dir + "#{jam_track.store_dir}/tracks" + end + + # create name of the file + def filename + track_type == 'Master' ? 'master.ogg' : "#{part}.ogg" + end + + # creates a short-lived URL that has access to the object. + # the idea is that this is used when a user who has the rights to this tries to download this JamTrack + # we would verify their rights (can_download?), and generates a URL in response to the click so that they can download + # but the url is short lived enough so that it wouldn't be easily shared + def sign_url(expiration_time = 120) + s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false}) + end + + def can_download?(user) + # I think we have to make a special case for 'previews', but maybe that's just up to the controller to not check can_download? + jam_track.owners.include?(user) + end + end +end diff --git a/ruby/lib/jam_ruby/models/recorded_track.rb b/ruby/lib/jam_ruby/models/recorded_track.rb index ccd5a2a3c..e11a76bf0 100644 --- a/ruby/lib/jam_ruby/models/recorded_track.rb +++ b/ruby/lib/jam_ruby/models/recorded_track.rb @@ -16,6 +16,7 @@ module JamRuby attr_writer :is_skip_mount_uploader attr_accessible :discard, :user, :user_id, :instrument_id, :sound, :client_id, :track_id, :client_track_id, :url, as: :admin + attr_writer :current_user SOUND = %w(mono stereo) diff --git a/ruby/lib/jam_ruby/models/search.rb b/ruby/lib/jam_ruby/models/search.rb index c83730172..97f6b80de 100644 --- a/ruby/lib/jam_ruby/models/search.rb +++ b/ruby/lib/jam_ruby/models/search.rb @@ -90,6 +90,7 @@ module JamRuby PARAM_MUSICIAN = :srch_m PARAM_BAND = :srch_b PARAM_FEED = :srch_f + PARAM_JAMTRACK = :srch_j F_PER_PAGE = B_PER_PAGE = M_PER_PAGE = 20 M_MILES_DEFAULT = 500 diff --git a/ruby/lib/jam_ruby/models/shopping_cart.rb b/ruby/lib/jam_ruby/models/shopping_cart.rb new file mode 100644 index 000000000..ccad1b9d4 --- /dev/null +++ b/ruby/lib/jam_ruby/models/shopping_cart.rb @@ -0,0 +1,34 @@ +module JamRuby + class ShoppingCart < ActiveRecord::Base + + attr_accessible :quantity, :cart_type, :product_info + + belongs_to :user, :inverse_of => :shopping_carts, :class_name => "JamRuby::User", :foreign_key => "user_id" + + validates :cart_id, presence: true + validates :cart_type, presence: true + validates :cart_class_name, presence: true + + default_scope order('created_at DESC') + + def product_info + product = self.cart_product + {name: product.name, price: product.price} unless product.nil? + end + + def cart_product + self.cart_class_name.classify.constantize.find_by_id self.cart_id unless self.cart_class_name.blank? + end + + def self.create user, product, quantity = 1 + cart = ShoppingCart.new + cart.user = user + cart.cart_type = product.class::PRODUCT_TYPE + cart.cart_class_name = product.class.name + cart.cart_id = product.id + cart.quantity = quantity + cart.save + cart + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 6e9e6fad0..07daa3e4d 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -127,6 +127,13 @@ module JamRuby # diagnostics has_many :diagnostics, :class_name => "JamRuby::Diagnostic" + # jam_tracks + has_many :jam_track_rights, :class_name => "JamRuby::JamTrackRight", :foreign_key => "user_id" + has_many :purchased_jam_tracks, :through => :jam_track_rights, :class_name => "JamRuby::JamTrack", :source => :jam_track, :order => :created_at + + # Shopping carts + has_many :shopping_carts, :class_name => "JamRuby::ShoppingCart" + # score history has_many :from_score_histories, :class_name => "JamRuby::ScoreHistory", foreign_key: 'from_user_id' has_many :to_score_histories, :class_name => "JamRuby::ScoreHistory", foreign_key: 'to_user_id' diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 264b44323..a044f9659 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -673,4 +673,54 @@ FactoryGirl.define do factory :max_mind_release, :class => JamRuby::MaxMindRelease do released_at Time.now.to_date end + + factory :jam_track_licensor, :class => JamRuby::JamTrackLicensor do + sequence(:name) { |n| "licensor-#{n}" } + sequence(:description) { |n| "description-#{n}" } + sequence(:attention) { |n| "attention-#{n}" } + sequence(:address_line_1) { |n| "address1-#{n}" } + sequence(:address_line_2) { |n| "address2-#{n}" } + sequence(:city) { |n| "city-#{n}" } + sequence(:state) { |n| "state-#{n}" } + sequence(:zip_code) { |n| "zipcode-#{n}" } + sequence(:contact) { |n| "contact-#{n}" } + sequence(:email) { |n| "email-#{n}" } + sequence(:phone) { |n| "phone-#{n}" } + end + + factory :jam_track, :class => JamRuby::JamTrack do + sequence(:name) { |n| "jam-track-#{n}" } + sequence(:description) { |n| "description-#{n}" } + bpm 100.1 + time_signature '4/4' + status 'Production' + recording_type 'Cover' + sequence(:original_artist) { |n| "original-artist-#{n}" } + sequence(:songwriter) { |n| "songwriter-#{n}" } + sequence(:publisher) { |n| "publisher-#{n}" } + pro 'ASCAP' + sales_region 'United States' + price 1.99 + reproduction_royalty true + public_performance_royalty true + reproduction_royalty_amount 0.999 + licensor_royalty_amount 0.999 + pro_royalty_amount 0.999 + + genre JamRuby::Genre.first + association :licensor, factory: :jam_track_licensor + end + + factory :jam_track_track, :class => JamRuby::JamTrackTrack do + position 1 + part 'lead guitar' + track_type 'Track' + instrument JamRuby::Instrument.find('electric guitar') + association :jam_track, factory: :jam_track + end + + factory :jam_track_right, :class => JamRuby::JamTrackRight do + association :jam_track, factory: :jam_track + association :user, factory: :user + end end diff --git a/ruby/spec/jam_ruby/models/jam_track_licensor_spec.rb b/ruby/spec/jam_ruby/models/jam_track_licensor_spec.rb new file mode 100644 index 000000000..80bfc377c --- /dev/null +++ b/ruby/spec/jam_ruby/models/jam_track_licensor_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' + +describe JamTrackLicensor do + + it "created" do + FactoryGirl.create(:jam_track_licensor) + end + +end + diff --git a/ruby/spec/jam_ruby/models/jam_track_right_spec.rb b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb new file mode 100644 index 000000000..5d136d62a --- /dev/null +++ b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe JamTrackRight do + + it "created" do + jam_track_right = FactoryGirl.create(:jam_track_right) + + user = jam_track_right.user + jam_track = jam_track_right.jam_track + + # verify that the user sees this as a purchased jam_track + user.purchased_jam_tracks.should == [jam_track] + + # verify that the jam_track sees the user as an owner + jam_track.owners.should == [user] + + end + + describe "validations" do + it "one purchase per user/jam_track combo" do + user = FactoryGirl.create(:user) + jam_track = FactoryGirl.create(:jam_track) + + right_1 = FactoryGirl.create(:jam_track_right, user: user, jam_track: jam_track) + right_2 = FactoryGirl.build(:jam_track_right, user: user, jam_track: jam_track) + right_2.valid?.should be_false + right_2.errors[:user_id].should == ['has already been taken'] + end + end +end + diff --git a/ruby/spec/jam_ruby/models/jam_track_spec.rb b/ruby/spec/jam_ruby/models/jam_track_spec.rb new file mode 100644 index 000000000..85cf9059c --- /dev/null +++ b/ruby/spec/jam_ruby/models/jam_track_spec.rb @@ -0,0 +1,138 @@ +require 'spec_helper' + +require 'carrierwave/test/matchers' + +describe JamTrack do + include CarrierWave::Test::Matchers + include UsesTempFiles + + + it "created" do + jam_track = FactoryGirl.create(:jam_track) + jam_track.licensor.should_not be_nil + jam_track.licensor.jam_tracks.should == [jam_track] + end + + describe "validations" do + describe "bpm" do + it "1" do + FactoryGirl.build(:jam_track, bpm: 1).valid?.should be_true + end + + it "100" do + FactoryGirl.build(:jam_track, bpm: 100).valid?.should be_true + end + + it "100.1" do + FactoryGirl.build(:jam_track, bpm: 100.1).valid?.should be_true + end + + it "100.12" do + jam_track = FactoryGirl.build(:jam_track, bpm: 100.12) + jam_track.valid?.should be_false + jam_track.errors[:bpm].should == ['is invalid'] + end + end + + describe "price" do + + it "0.99" do + FactoryGirl.build(:jam_track, price: 0.99).valid?.should be_true + end + + it "1" do + FactoryGirl.build(:jam_track, price: 1).valid?.should be_true + end + + it "100" do + FactoryGirl.build(:jam_track, price: 100).valid?.should be_true + end + + it "100.1" do + FactoryGirl.build(:jam_track, price: 100.1).valid?.should be_true + end + + it "100.12" do + FactoryGirl.build(:jam_track, price: 100.12).valid?.should be_true + end + + it "100.123" do + jam_track = FactoryGirl.build(:jam_track, price: 100.123) + jam_track.valid?.should be_false + jam_track.errors[:price].should == ['is invalid'] + end + end + + describe "reproduction_royalty_amount" do + it "0.99" do + FactoryGirl.build(:jam_track, reproduction_royalty_amount: 0.99).valid?.should be_true + end + + it "1" do + FactoryGirl.build(:jam_track, reproduction_royalty_amount: 1).valid?.should be_true + end + + it "100" do + FactoryGirl.build(:jam_track, reproduction_royalty_amount: 100).valid?.should be_true + end + + it "100.1" do + FactoryGirl.build(:jam_track, reproduction_royalty_amount: 100.1).valid?.should be_true + end + + it "100.12" do + FactoryGirl.build(:jam_track, reproduction_royalty_amount: 100.12).valid?.should be_true + end + + it "100.123" do + FactoryGirl.build(:jam_track, reproduction_royalty_amount: 100.123).valid?.should be_true + end + + it "100.1234" do + jam_track = FactoryGirl.build(:jam_track, reproduction_royalty_amount: 100.1234) + jam_track.valid?.should be_false + jam_track.errors[:reproduction_royalty_amount].should == ['is invalid'] + end + end + end + + describe "upload/download" do + JKA_NAME = 'blah.jka' + + in_directory_with_file(JKA_NAME) + + before(:all) do + original_storage = JamTrackUploader.storage = :fog + end + + after(:all) do + JamTrackUploader.storage = @original_storage + end + + before(:each) do + content_for_file('abc') + end + + it "uploads to s3 with correct name, and then downloads via signed URL" do + jam_track = FactoryGirl.create(:jam_track) + uploader = JamTrackUploader.new(jam_track, :url) + uploader.store!(File.open(JKA_NAME)) # uploads file + jam_track.save! + + # verify that the uploader stores the correct path + jam_track[:url].should == jam_track.store_dir + '/' + jam_track.filename + + # verify it's on S3 + s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + s3.exists?(jam_track[:url]).should be_true + s3.length(jam_track[:url]).should == 'abc'.length + + # download it via signed URL, and check contents + url = jam_track.sign_url + downloaded_contents = open(url).read + downloaded_contents.should == 'abc' + end + + end +end + diff --git a/ruby/spec/jam_ruby/models/jam_track_track_spec.rb b/ruby/spec/jam_ruby/models/jam_track_track_spec.rb new file mode 100644 index 000000000..6cc5c3773 --- /dev/null +++ b/ruby/spec/jam_ruby/models/jam_track_track_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +describe JamTrackTrack do + include CarrierWave::Test::Matchers + include UsesTempFiles + + it "created" do + jam_track_track = FactoryGirl.create(:jam_track_track) + jam_track_track.jam_track.should_not be_nil + jam_track_track.jam_track.jam_track_tracks.should == [jam_track_track] + end + + describe "validations" do + it "position" do + jam_track = FactoryGirl.create(:jam_track) + jam_track_track_1 = FactoryGirl.create(:jam_track_track, position: 1, jam_track: jam_track) + jam_track_track_2 = FactoryGirl.build(:jam_track_track, position: 1, jam_track: jam_track) + jam_track_track_2.valid?.should == false + jam_track_track_2.errors[:position].should == ['has already been taken'] + end + + it "jam_track required" do + pending "Need to be not mandatory because of activeadmin" + jam_track = FactoryGirl.build(:jam_track_track, jam_track: nil) + jam_track.valid?.should be_false + jam_track.errors[:jam_track].should == ["can't be blank"] + end + end + + + describe "upload/download" do + TRACK_NAME = 'lead guitar.ogg' + + in_directory_with_file(TRACK_NAME) + + before(:all) do + original_storage = JamTrackTrackUploader.storage = :fog + end + + after(:all) do + JamTrackTrackUploader.storage = @original_storage + end + + before(:each) do + content_for_file('abc') + end + + it "uploads to s3 with correct name, and then downloads via signed URL" do + jam_track_track = FactoryGirl.create(:jam_track_track) + uploader = JamTrackTrackUploader.new(jam_track_track, :url) + uploader.store!(File.open(TRACK_NAME)) # uploads file + jam_track_track.save! + + # verify that the uploader stores the correct path + jam_track_track[:url].should == jam_track_track.store_dir + '/' + jam_track_track.filename + + # verify it's on S3 + s3 = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + s3.exists?(jam_track_track[:url]).should be_true + s3.length(jam_track_track[:url]).should == 'abc'.length + + # download it via signed URL, and check contents + url = jam_track_track.sign_url + downloaded_contents = open(url).read + downloaded_contents.should == 'abc' + end + + end + +end diff --git a/ruby/spec/jam_ruby/models/shopping_cart_spec.rb b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb new file mode 100644 index 000000000..db3f4d75c --- /dev/null +++ b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe ShoppingCart do + + let(:user) { FactoryGirl.create(:user) } + let(:jam_track) {FactoryGirl.create(:jam_track) } + + before(:each) do + ShoppingCart.delete_all + end + + it "can reference a shopping cart" do + shopping_cart = ShoppingCart.create user, jam_track, 1 + + ShoppingCart.count.should == 1 + user.shopping_carts.count.should == 1 + user.shopping_carts[0].product_info[:name].should == jam_track.name + user.shopping_carts[0].product_info[:price].should == jam_track.price + user.shopping_carts[0].cart_type.should == jam_track.class::PRODUCT_TYPE + user.shopping_carts[0].quantity.should == 1 + end + +end diff --git a/ruby/spec/jam_ruby/resque/unused_music_notation_cleaner_spec.rb b/ruby/spec/jam_ruby/resque/unused_music_notation_cleaner_spec.rb index f47f186df..1f0ff3e55 100644 --- a/ruby/spec/jam_ruby/resque/unused_music_notation_cleaner_spec.rb +++ b/ruby/spec/jam_ruby/resque/unused_music_notation_cleaner_spec.rb @@ -5,9 +5,9 @@ require 'fileutils' describe UnusedMusicNotationCleaner do include UsesTempFiles - NOTATION_TEMP_FILE='detail.png' + UNUSED_NOTATION_TEMP_FILE='detail.png' - in_directory_with_file(NOTATION_TEMP_FILE) + in_directory_with_file(UNUSED_NOTATION_TEMP_FILE) before do content_for_file("this is music notation test file") @@ -30,7 +30,8 @@ describe UnusedMusicNotationCleaner do it "find no music notataions if music_session_id is nil and created at 1 hour ago" do notation = MusicNotation.new - notation.file_url = File.open(NOTATION_TEMP_FILE) + + notation.file_url = File.open(UNUSED_NOTATION_TEMP_FILE) notation.size = 10 notation.user = FactoryGirl.create(:user) notation.created_at = Time.now - 1.hours @@ -44,7 +45,7 @@ describe UnusedMusicNotationCleaner do music_session = FactoryGirl.create(:music_session, :session_removed_at => Time.now - 1.hours) notation = MusicNotation.new - notation.file_url = File.open(NOTATION_TEMP_FILE) + notation.file_url = File.open(UNUSED_NOTATION_TEMP_FILE) notation.size = 10 notation.user = FactoryGirl.create(:user) notation.created_at = Time.now - 1.hours @@ -56,7 +57,7 @@ describe UnusedMusicNotationCleaner do it "find music notataions if music_session_id is nil and created at 2 days ago" do notation = MusicNotation.new - notation.file_url = File.open(NOTATION_TEMP_FILE) + notation.file_url = File.open(UNUSED_NOTATION_TEMP_FILE) notation.size = 10 notation.user = FactoryGirl.create(:user) notation.created_at = Time.now - 2.days @@ -70,7 +71,7 @@ describe UnusedMusicNotationCleaner do music_session = FactoryGirl.create(:music_session, :session_removed_at => Time.now - 2.days) notation = MusicNotation.new - notation.file_url = File.open(NOTATION_TEMP_FILE) + notation.file_url = File.open(UNUSED_NOTATION_TEMP_FILE) notation.size = 10 notation.user = FactoryGirl.create(:user) notation.created_at = Time.now - 3.days diff --git a/web/Gemfile b/web/Gemfile index e2b8e8a44..816dbc6c1 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -83,6 +83,8 @@ gem 'rubyzip' gem 'slim' gem 'htmlentities' gem 'sanitize' +gem 'recurly' +gem 'guard', '2.7.3' group :development, :test do gem 'rspec-rails', '2.14.2' diff --git a/web/app/assets/images/content/bkg_home_jamtracks.jpg b/web/app/assets/images/content/bkg_home_jamtracks.jpg new file mode 100644 index 000000000..700f012c6 Binary files /dev/null and b/web/app/assets/images/content/bkg_home_jamtracks.jpg differ diff --git a/web/app/assets/images/content/bkg_home_jamtracks_x.jpg b/web/app/assets/images/content/bkg_home_jamtracks_x.jpg new file mode 100644 index 000000000..22df61a3d Binary files /dev/null and b/web/app/assets/images/content/bkg_home_jamtracks_x.jpg differ diff --git a/web/app/assets/images/content/checkmark.png b/web/app/assets/images/content/checkmark.png new file mode 100644 index 000000000..8ce5e42ee Binary files /dev/null and b/web/app/assets/images/content/checkmark.png differ diff --git a/web/app/assets/images/content/icon_jamtracks.png b/web/app/assets/images/content/icon_jamtracks.png new file mode 100644 index 000000000..4cdb1e1f5 Binary files /dev/null and b/web/app/assets/images/content/icon_jamtracks.png differ diff --git a/web/app/assets/images/content/icon_shopping_cart.png b/web/app/assets/images/content/icon_shopping_cart.png new file mode 100644 index 000000000..24bf9b09f Binary files /dev/null and b/web/app/assets/images/content/icon_shopping_cart.png differ diff --git a/web/app/assets/images/content/shopping-cart.png b/web/app/assets/images/content/shopping-cart.png new file mode 100644 index 000000000..eef1a4c69 Binary files /dev/null and b/web/app/assets/images/content/shopping-cart.png differ diff --git a/web/app/assets/javascripts/checkout_signin.js b/web/app/assets/javascripts/checkout_signin.js new file mode 100644 index 000000000..beb536d3d --- /dev/null +++ b/web/app/assets/javascripts/checkout_signin.js @@ -0,0 +1,106 @@ +(function(context,$) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.CheckoutSignInScreen = function(app) { + + var logger = context.JK.logger; + + var $screen = null; + var $navigation = null; + var $signinForm = null; + var $self = $(this); + var $email = null; + var $password = null; + var $signinBtn = null; + var $signupBtn = null; + + function beforeShow(data) { + renderNavigation(); + } + + function afterShow(data) { + } + + function events() { + $signinBtn.on('click', login); + $signupBtn.on('click', signup); + } + + function signup(e) { + app.layout.showDialog('signup-dialog'); + return false; + } + + function reset() { + $signinForm.removeClass('login-error'); + + $email.val(''); + $password.val(''); + } + + function login() { + var email = $email.val(); + var password = $password.val(); + + reset(); + + $signinBtn.text('TRYING...'); + + rest.login({email: email, password: password, remember_me: false}) + .done(function() { + window.location = '/client#/order' + }) + .fail(function(jqXHR) { + if(jqXHR.status == 422) { + $signinForm.addClass('login-error') + } + else { + app.notifyServerError(jqXHR, "Unable to log in") + } + }) + .always(function() { + $signinBtn.text('SIGN IN') + }) + } + + function renderNavigation() { + $navigation.html(""); + + var navigationHtml = $( + context._.template( + $('#template-checkout-navigation').html(), + {current: 1}, + {variable: 'data'} + ) + ); + + $navigation.append(navigationHtml); + } + + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow + }; + app.bindScreen('signin', screenBindings); + + $screen = $("#signInScreen"); + $navigation = $screen.find(".checkout-navigation-bar"); + $signinForm = $screen.find(".signin-form"); + $signinBtn = $signinForm.find('.signin-submit'); + $email = $signinForm.find('input[name="session[email]"]'); + $password = $signinForm.find('input[name="session[password]"]'); + $signupBtn = $signinForm.find('.show-signup-dialog'); + + if($screen.length == 0) throw "$screen must be specified"; + if($navigation.length == 0) throw "$navigation must be specified"; + + events(); + } + + this.initialize = initialize; + + return this; + } +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/dialog/banner.js b/web/app/assets/javascripts/dialog/banner.js index 991d14d67..2ced92f0c 100644 --- a/web/app/assets/javascripts/dialog/banner.js +++ b/web/app/assets/javascripts/dialog/banner.js @@ -62,7 +62,7 @@ } hide(); - + logger.debug("opening banner:" + options.title); var $h1 = $banner.find('h1'); diff --git a/web/app/assets/javascripts/dialog/jamtrackAvailabilityDialog.js b/web/app/assets/javascripts/dialog/jamtrackAvailabilityDialog.js new file mode 100644 index 000000000..c900cf54c --- /dev/null +++ b/web/app/assets/javascripts/dialog/jamtrackAvailabilityDialog.js @@ -0,0 +1,46 @@ +(function(context,$) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.JamtrackAvailabilityDialog = function(app) { + var logger = context.JK.logger; + var $dialog = null; + var dialogId = 'jamtrack-availability-dialog'; + + function beforeShow(data) { + } + + function afterShow(data) { + } + + function afterHide() { + } + + function showDialog() { + return app.layout.showDialog(dialogId); + } + + function events() { + } + + function initialize() { + + var dialogBindings = { + 'beforeShow' : beforeShow, + 'afterShow' : afterShow, + 'afterHide': afterHide + }; + + app.bindDialog(dialogId, dialogBindings); + + $dialog = $('[layout-id="' + dialogId + '"]'); + + events(); + } + + this.initialize = initialize; + this.showDialog = showDialog; + }; + + return this; +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 47368278c..ad1993899 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -238,6 +238,10 @@ } function cancelRsvpRequest(sessionId, rsvpRequestId, cancelAll) { + var cancel = "yes"; + if (cancelAll) { + cancel = "all"; + } return $.ajax({ url: '/api/rsvp_requests/' + rsvpRequestId, type: "DELETE", @@ -939,7 +943,11 @@ dataType: "json", contentType: 'application/json', url: "/api/users/progression/certified_gear", - data: JSON.stringify(options) + processData: false, + data: JSON.stringify({ + success: options.success, + reason: options.reason + }) }); } @@ -1257,6 +1265,98 @@ }); } + function updateAudioLatency(options) { + var id = getId(options); + return $.ajax({ + type: "POST", + url: '/api/users/' + id + '/audio_latency', + dataType: "json", + contentType: 'application/json', + data: options, + }); + } + + function getJamtracks(options) { + return $.ajax({ + type: "GET", + url: '/api/jamtracks?' + $.param(options), + dataType: "json", + contentType: 'application/json' + }); + } + + function addJamtrackToShoppingCart(options) { + return $.ajax({ + type: "POST", + url: '/api/shopping_carts/add_jamtrack?' + $.param(options), + dataType: "json", + contentType: 'applications/json' + }); + } + + function getShoppingCarts() { + return $.ajax({ + type: "GET", + url: '/api/shopping_carts', + dataType: "json", + contentType: 'application/json' + }); + } + + function removeShoppingCart(options) { + return $.ajax({ + type: "DELETE", + url: '/api/shopping_carts?' + $.param(options), + dataType: "json", + contentType: 'application/json' + }) + } + + function getRecurlyAccount() { + return $.ajax({ + type: "GET", + url: '/api/recurly/get_account', + dataType: "json", + contentType: 'application/json' + }); + } + + function createRecurlyAccount(options) { + return $.ajax({ + type: "POST", + url: '/api/recurly/create_account?' + $.param(options), + dataType: "json", + contentType: 'application/json' + }); + } + + function getBillingInfo() { + return $.ajax({ + type: "GET", + url: '/api/recurly/billing_info', + dataType: "json", + contentType: 'application/json' + }); + } + + function updateBillingInfo(options) { + return $.ajax({ + type: "PUT", + url: '/api/recurly/update_billing_info?' + $.param(options), + dataType: "json", + contentType: 'application/json' + }); + } + + function placeOrder(options) { + return $.ajax({ + type: "PUT", + url: '/api/recurly/place_order?' + $.param(options), + dataType: "json", + contentType: 'application/json' + }); + } + function searchMusicians(query) { return $.ajax({ type: "GET", @@ -1375,6 +1475,16 @@ this.getChatMessages = getChatMessages; this.createDiagnostic = createDiagnostic; this.getLatencyTester = getLatencyTester; + this.updateAudioLatency = updateAudioLatency; + this.getJamtracks = getJamtracks; + this.addJamtrackToShoppingCart = addJamtrackToShoppingCart; + this.getShoppingCarts = getShoppingCarts; + this.removeShoppingCart = removeShoppingCart; + this.getRecurlyAccount = getRecurlyAccount; + this.createRecurlyAccount = createRecurlyAccount; + this.getBillingInfo = getBillingInfo; + this.updateBillingInfo = updateBillingInfo; + this.placeOrder = placeOrder; this.searchMusicians = searchMusicians; return this; diff --git a/web/app/assets/javascripts/jamtrack.js b/web/app/assets/javascripts/jamtrack.js new file mode 100644 index 000000000..6ae767514 --- /dev/null +++ b/web/app/assets/javascripts/jamtrack.js @@ -0,0 +1,259 @@ +(function(context,$) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.JamTrackScreen = function(app) { + + var logger = context.JK.logger; + + var $screen = null; + var $content = null; + var $scroller = null; + var $genre = null; + var $instrument = null; + var $availability = null; + var $nextPager = null; + var $noMoreJamtracks = null; + + var currentQuery = defaultQuery(); + var currentPage = 0; + var LIMIT = 10; + var next = null; + var instrument_logo_map = context.JK.getInstrumentIconMap24(); + + function beforeShow(data) { + refresh(); + } + + function afterShow(data) { + } + + function events() { + $genre.on("change", search); + $instrument.on("change", search); + $availability.on("change", search); + } + + function clearResults() { + //logger.debug("CLEARING CONTENT") + currentPage = 0; + $content.empty(); + $noMoreJamtracks.hide(); + next = null; + } + + function refresh() { + currentQuery = buildQuery(); + rest.getJamtracks(currentQuery) + .done(function(response) { + clearResults(); + handleJamtrackResponse(response); + }) + .fail(function(jqXHR) { + clearResults(); + $noMoreJamtracks.show(); + app.notifyServerError(jqXHR, 'Jamtrack Unavailable') + }) + } + + function search() { + logger.debug("Searching for jamtracks..."); + refresh(); + return false; + } + + function defaultQuery() { + var query = { limit:LIMIT, page:currentPage}; + + if(next) { + query.since = next; + } + + return query; + } + + function buildQuery() { + currentQuery = defaultQuery(); + + // genre filter + var genres = $screen.find('#jamtrack_genre').val(); + if (genres !== undefined) { + currentQuery.genre = genres; + } + + // instrument filter + var instrument = $instrument.val(); + if (instrument !== undefined) { + currentQuery.instrument = instrument; + } + + // availability filter + var availability = $availability.val(); + if (availability !== undefined) { + currentQuery.availability = availability; + } + + return currentQuery; + } + + function handleJamtrackResponse(response) { + //logger.debug("Handling response", JSON.stringify(response)) + next = response.next; + + renderJamtracks(response); + + if(response.next == null) { + // if we less results than asked for, end searching + $scroller.infinitescroll('pause'); + logger.debug("end of jamtracks"); + + if(currentPage == 0 && response.jamtracks.length == 0) { + $content.append("
+
+