diff --git a/admin/.rakeTasks b/admin/.rakeTasks
new file mode 100644
index 000000000..c6865d9a1
--- /dev/null
+++ b/admin/.rakeTasks
@@ -0,0 +1,7 @@
+
+
diff --git a/admin/app/admin/jam_track_licensors.rb b/admin/app/admin/jam_track_licensors.rb
new file mode 100644
index 000000000..65fdaed6d
--- /dev/null
+++ b/admin/app/admin/jam_track_licensors.rb
@@ -0,0 +1,8 @@
+ActiveAdmin.register JamRuby::JamTrackLicensor, :as => 'JamTrack Licensors' do
+
+ menu :label => 'JamTrack Licensors', :parent => 'JamTracks'
+
+ config.sort_order = 'name_asc'
+ config.batch_actions = false
+
+end
\ No newline at end of file
diff --git a/admin/app/admin/jam_tracks.rb b/admin/app/admin/jam_tracks.rb
new file mode 100644
index 000000000..0f6d99e08
--- /dev/null
+++ b/admin/app/admin/jam_tracks.rb
@@ -0,0 +1,57 @@
+ActiveAdmin.register JamRuby::JamTrack, :as => 'JamTracks' do
+
+ menu :label => 'JamTracks', :parent => 'JamTracks'
+
+ config.sort_order = 'name_asc'
+ config.batch_actions = false
+
+ filter :genre
+ filter :status, :as => :select, collection: JamRuby::JamTrack::STATUS
+
+ form :partial => 'form'
+
+ index do
+ column :id
+ column :name
+ column :description
+ column :bpm
+ column :time_signature
+ column :status
+ column :recording_type
+ column :original_artist
+ column :songwriter
+ column :publisher
+ column :licensor
+ column :pro
+ column :genre
+ column :sales_region
+ column :price
+ column :reproduction_royalty
+ column :public_performance_royalty
+ column :reproduction_royalty_amount
+ column :licensor_royalty_amount
+ column :pro_royalty_amount
+ column :url
+ column :created_at
+
+ column :jam_track_tracks do |jam_track|
+ table_for jam_track.jam_track_tracks.order('position ASC') do
+ column :id
+ column :track_type
+ column :instrument
+ column :part
+ column :track do |track|
+ link_to 'Play', '#'
+ end
+ end
+ end
+
+ # default_actions # use this for all view/edit/delete links
+ column "Actions" do |jam_track|
+ links = ''.html_safe
+ links << link_to("Show Tracks", '#', :class => "member_link view_link show_tracks")
+ links << link_to("Update", edit_resource_path(jam_track), :class => "member_link edit_link")
+ links
+ end
+ end
+end
\ No newline at end of file
diff --git a/admin/app/assets/javascripts/jam_track.js b/admin/app/assets/javascripts/jam_track.js
new file mode 100644
index 000000000..a8781672a
--- /dev/null
+++ b/admin/app/assets/javascripts/jam_track.js
@@ -0,0 +1,43 @@
+$(document).ready(function() {
+ $("th.jam_track_tracks").css('display', 'none');
+ $("td.jam_track_tracks").css('display', 'none');
+
+ $(".show_tracks").click(function(e) {
+ e.preventDefault();
+
+ var $rowJamTrack = $(this).parents('tr');
+ var $jamTrackTracks = $($rowJamTrack).find("td.jam_track_tracks");
+
+ var count = $jamTrackTracks.find("table tbody tr").length;
+
+ if ($rowJamTrack.next().attr('id') == "jam_track_tracks_detail") {
+ $(this).html("Show Tracks");
+ $rowJamTrack.next().remove();
+ }
+ else {
+ $(this).html('Hide Tracks');
+ if (count == 0) {
+ $rowJamTrack.after(
+ $("
").html(
+ $(" | ")
+ ).append(
+ $(" | ").html(
+ "No Tracks"
+ )
+ )
+ );
+ }
+ else {
+ $rowJamTrack.after(
+ $("
").html(
+ $(" | ")
+ ).append(
+ $(" | ").html(
+ $jamTrackTracks.html()
+ )
+ )
+ );
+ }
+ }
+ })
+});
\ No newline at end of file
diff --git a/admin/app/views/admin/jam_tracks/_form.html.haml b/admin/app/views/admin/jam_tracks/_form.html.haml
new file mode 100644
index 000000000..5c2028673
--- /dev/null
+++ b/admin/app/views/admin/jam_tracks/_form.html.haml
@@ -0,0 +1,31 @@
+= semantic_form_for([:admin, resource], :html => {:multipart => true}, :url => resource.new_record? ? admin_jam_tracks_path : "#{ENV['RAILS_RELATIVE_URL_ROOT']}/admin/jam_tracks/#{resource.id}") do |f|
+ = f.semantic_errors *f.object.errors.keys
+ = f.inputs name: 'JamTrack fields' do
+
+ = f.input :name
+ = f.input :description
+ = f.input :bpm
+ = f.input :time_signature, collection: JamRuby::JamTrack::TIME_SIGNATURES
+ = f.input :status, collection: JamRuby::JamTrack::STATUS
+ = f.input :recording_type, collection: JamRuby::JamTrack::RECORDING_TYPE
+ = f.input :original_artist
+ = f.input :songwriter
+ = f.input :publisher
+ = f.input :licensor, collection: JamRuby::JamTrackLicensor.all
+ = f.input :pro, collection: JamRuby::JamTrack::PRO
+ = f.input :genre, collection: JamRuby::Genre.all
+ = f.input :sales_region, collection: JamRuby::JamTrack::SALES_REGION
+ = f.input :price
+ = f.input :reproduction_royalty, :label => 'Reproduction Royalty'
+ = f.input :public_performance_royalty, :label => 'Public Performance Royalty'
+ = f.input :reproduction_royalty_amount
+ = f.input :licensor_royalty_amount
+ = f.input :pro_royalty_amount
+ = f.input :url, :as => :file, :label => 'Audio File'
+
+ = f.semantic_fields_for :jam_track_tracks do |track|
+ = render 'jam_track_track_fields', f: track
+ .links
+ = link_to_add_association 'Add Track', f, :jam_track_tracks, class: 'button', style: 'margin:20px;padding:10px 20px'
+
+ = f.actions
\ No newline at end of file
diff --git a/admin/app/views/admin/jam_tracks/_jam_track_track_fields.html.haml b/admin/app/views/admin/jam_tracks/_jam_track_track_fields.html.haml
new file mode 100644
index 000000000..11200771b
--- /dev/null
+++ b/admin/app/views/admin/jam_tracks/_jam_track_track_fields.html.haml
@@ -0,0 +1,18 @@
+= f.inputs name: 'Track fields' do
+
+ %ol.nested-fields
+ = f.input :track_type, :as => :select, collection: JamRuby::JamTrackTrack::TRACK_TYPE
+ = f.input :instrument, collection: Instrument.all
+ = f.input :part
+ = f.input :position
+
+ - if f.object.new_record?
+ %p{style: 'margin-left:10px'}
+ %i before you can upload, you must select 'Update JamTrack'
+ - else
+ = f.input :url, :as => :file, :label => 'Track file'
+ - unless f.object.nil? || f.object[:url].nil?
+ .current_file_holder{style: 'margin-bottom:10px'}
+ %a{href: f.object.sign_url(3600), style: 'padding:0 0 0 20px'} Download
+
+ = link_to_remove_association "Delete Track", f, class: 'button', style: 'margin-left:10px'
\ No newline at end of file
diff --git a/admin/config/application.rb b/admin/config/application.rb
index 08b11d58c..1b9485036 100644
--- a/admin/config/application.rb
+++ b/admin/config/application.rb
@@ -134,5 +134,10 @@ module JamAdmin
# recording upload/download configs
config.max_track_upload_failures = 10
config.max_track_part_upload_failures = 3
+
+ # Use Private API Keys to communicate with Recurly's API v2. See https://docs.recurly.com/api/basics/authentication to learn more.
+ config.recurly_private_api_key = '7d623daabfc2434fa2a893bb008eb3e6'
+ # Use Public Keys to identify your site when using Recurly.js. See https://docs.recurly.com/js/#include to learn more.
+ config.recurly_public_api_key = 'sc-SZlO11shkeA1WMGuISLGg5'
end
end
diff --git a/admin/config/environments/test.rb b/admin/config/environments/test.rb
index 1f9229001..33880f53b 100644
--- a/admin/config/environments/test.rb
+++ b/admin/config/environments/test.rb
@@ -42,4 +42,9 @@ JamAdmin::Application.configure do
config.twitter_app_secret = 'PfG1jAUMnyrimPcDooUVQaJrG1IuDjUyGg5KciOo'
config.redis_host = "localhost:6379:1" # go to another db to not cross pollute into dev/production redis dbs
+
+ # Use Private API Keys to communicate with Recurly's API v2. See https://docs.recurly.com/api/basics/authentication to learn more.
+ config.recurly_private_api_key = '4631527f203b41848523125b3ae51341'
+ # Use Public Keys to identify your site when using Recurly.js. See https://docs.recurly.com/js/#include to learn more.
+ config.recurly_public_api_key = 'sc-s6G2OA80Rwyvsb1RmS3mAE'
end
diff --git a/admin/spec/factories.rb b/admin/spec/factories.rb
index 3ba6c50aa..08f668ff3 100644
--- a/admin/spec/factories.rb
+++ b/admin/spec/factories.rb
@@ -205,4 +205,54 @@ FactoryGirl.define do
latency_tester.save
end
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/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/band_setup.js b/web/app/assets/javascripts/band_setup.js
index efe25c42b..dccd67937 100644
--- a/web/app/assets/javascripts/band_setup.js
+++ b/web/app/assets/javascripts/band_setup.js
@@ -22,6 +22,7 @@
var nilOptionText = 'n/a';
var bandId = '';
var friendInput=null;
+ var isSaving = false;
function is_new_record() {
return bandId.length == 0;
@@ -126,16 +127,21 @@
}
function saveBand() {
+ if (isSaving) return;
+ isSaving = true;
+
var band = buildBand()
if (is_new_record()) {
rest.createBand(band)
.done(function (response) {
+ isSaving = false;
createBandInvitations(response.id, function () {
context.location = "/client#/bandProfile/" + response.id;
});
})
.fail(function (jqXHR) {
+ isSaving = false;
app.notifyServerError(jqXHR, "Unable to create band")
});
;
@@ -144,11 +150,13 @@
band.id = bandId;
rest.updateBand(band)
.done(function (response) {
+ isSaving = false;
createBandInvitations(band.id, function () {
context.location = "/client#/bandProfile/" + band.id;
});
})
.fail(function (jqXHR) {
+ isSaving = false;
app.notifyServerError(jqXHR, "Unable to create band")
});
}
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("There's no jamtracks.
") ;
+ }
+
+ if(currentPage > 0) {
+ $noMoreJamtracks.show();
+ // there are bugs with infinitescroll not removing the 'loading'.
+ // it's most noticeable at the end of the list, so whack all such entries
+ $('.infinite-scroll-loader').remove();
+ }
+ }
+ else {
+ currentPage++;
+ buildQuery();
+ registerInfiniteScroll();
+ }
+ }
+
+ function registerInfiniteScroll() {
+ $scroller.infinitescroll({
+ behavior: 'local',
+ navSelector: '#jamtrackScreen .btn-next-pager',
+ nextSelector: '#jamtrackScreen .btn-next-pager',
+ binder: $scroller,
+ dataType: 'json',
+ appendCallback: false,
+ prefill: false,
+ bufferPx: 100,
+ loading: {
+ msg: $('Loading ...
'),
+ img: '/assets/shared/spinner.gif'
+ },
+ path: function(page) {
+ return '/api/jamtracks?' + $.param(buildQuery());
+ }
+ },function(json, opts) {
+ handleJamtrackResponse(json);
+ });
+ $scroller.infinitescroll('resume');
+ }
+
+ function playJamtrack(e) {
+ e.preventDefault();
+ }
+
+ function addToCartJamtrack(e) {
+ e.preventDefault();
+
+ var params = {id: $(e.target).attr("data-jamtrack-id")};
+
+ rest.addJamtrackToShoppingCart(params)
+ .done(function(response) {
+ context.location = "/client#/shoppingCart";
+ })
+ .fail(app.ajaxError);
+ }
+
+ function licenseUSWhy(e) {
+ e.preventDefault();
+
+ app.layout.showDialog('jamtrack-availability-dialog');
+ }
+
+ function registerEvents() {
+ $screen.find('.jamtrack-detail-btn').on("click", showJamtrackDescription);
+ $screen.find('.play-button').on('click', playJamtrack);
+ $screen.find('.jamtrack-add-cart').on('click', addToCartJamtrack);
+ $screen.find('.license-us-why').on('click', licenseUSWhy);
+ }
+
+ function renderJamtracks(data) {
+ $.each(data.jamtracks, function(i, jamtrack) {
+ $.each(jamtrack.tracks, function (index, track) {
+ var inst = '../assets/content/icon_instrument_default24.png';
+ if (track.instrument.id in instrument_logo_map) {
+ inst = instrument_logo_map[track.instrument.id].asset;
+ }
+ track.instrument_url = inst;
+
+ track.instrument_desc = track.instrument.description;
+ if (track.part != "") {
+ track.instrument_desc += " ( " + track.part + " )";
+ }
+ });
+
+ var options = {
+ jamtrack: jamtrack
+ };
+
+ var $jamtrackItem = $(
+ context._.template(
+ $('#template-jamtrack').html(),
+ options,
+ {variable: 'data'}
+ )
+ );
+ renderJamtrack($jamtrackItem );
+ });
+
+ registerEvents();
+ }
+
+ function showJamtrackDescription(e) {
+ e.preventDefault();
+
+ var $description = $(e.target).parent(".detail-arrow").next();
+ if ($description.css("display") == "none") {
+ $description.show();
+ }
+ else {
+ $description.hide();
+ }
+ }
+
+ function renderJamtrack(jamtrack) {
+ $content.append(jamtrack);
+ }
+
+ function initialize() {
+ var screenBindings = {
+ 'beforeShow': beforeShow,
+ 'afterShow': afterShow
+ };
+ app.bindScreen('jamtrack', screenBindings);
+
+ $screen = $("#jamtrack-find-form");
+ $scroller = $screen.find('.content-body-scroller');
+ $content = $screen.find(".jamtrack-content");
+ $genre = $screen.find("#jamtrack_genre");
+ $instrument = $screen.find("#jamtrack_instrument");
+ $availability = $screen.find("#jamtrack_availability");
+ $nextPager = $screen.find("a.btn-next-pager");
+ $noMoreJamtracks = $screen.find("#end-of-jamtrack-list");
+
+ if($screen.length == 0) throw "$screen must be specified";
+ if($scroller.length == 0) throw "$scroller must be specified";
+ if($content.length == 0) throw "$content must be specified";
+ if($noMoreJamtracks.length == 0) throw "$noMoreJamtracks must be specified";
+ if($genre.length == 0) throw "$genre must be specified";
+ if($instrument.length == 0) throw "$instrument must be specified";
+ if($availability.length ==0) throw "$availability must be specified";
+
+ events();
+ }
+
+ this.initialize = initialize;
+
+ return this;
+ }
+})(window,jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/order.js b/web/app/assets/javascripts/order.js
new file mode 100644
index 000000000..b6a78bff9
--- /dev/null
+++ b/web/app/assets/javascripts/order.js
@@ -0,0 +1,511 @@
+(function(context,$) {
+
+ "use strict";
+ context.JK = context.JK || {};
+ context.JK.OrderScreen = function(app) {
+
+ var logger = context.JK.logger;
+
+ var $screen = null;
+ var $navigation = null;
+ var $billingInfo = null;
+ var $shippingInfo = null;
+ var $paymentMethod = null;
+ var $shippingAddress = null;
+ var $shippingAsBilling = null;
+ var $paymentInfoPanel = null;
+ var $orderPanel = null;
+ var $orderContent = null;
+ var userDetail = null;
+ var step = null;
+ var billing_info = null;
+ var shipping_info = null;
+ var shipping_as_billing = null;
+
+ function beforeShow() {
+ beforeShowPaymentInfo();
+// moveToOrder();
+ }
+
+ function beforeShowPaymentInfo() {
+ step = 2;
+ renderNavigation();
+ renderAccountInfo();
+ }
+
+ function renderAccountInfo() {
+ rest.getUserDetail()
+ .done(populateAccountInfo)
+ .error(app.ajaxError);
+ }
+
+ function populateAccountInfo(user) {
+ userDetail = user;
+
+ if (userDetail.has_recurly_account) {
+ rest.getBillingInfo()
+ .done(function(response) {
+ $billingInfo.find("#billing-first-name").val(response.first_name);
+ $billingInfo.find("#billing-last-name").val(response.last_name);
+ $billingInfo.find("#billing-address1").val(response.address1);
+ $billingInfo.find("#billing-address2").val(response.address2);
+ $billingInfo.find("#billing-city").val(response.city);
+ $billingInfo.find("#billing-state").val(response.state);
+ $billingInfo.find("#billing-zip").val(response.zip);
+ $billingInfo.find("#billing-country").val(response.country);
+
+ $shippingAddress.find("#shipping-first-name").val(response.first_name);
+ $shippingAddress.find("#shipping-last-name").val(response.last_name);
+ $shippingAddress.find("#shipping-address1").val(response.address1);
+ $shippingAddress.find("#shipping-address2").val(response.address2);
+ $shippingAddress.find("#shipping-city").val(response.city);
+ $shippingAddress.find("#shipping-state").val(response.state);
+ $shippingAddress.find("#shipping-zip").val(response.zip);
+ $shippingAddress.find("#shipping-country").val(response.country);
+ })
+ .error(app.ajaxError);
+ }
+ else {
+ $billingInfo.find("#billing-first-name").val(userDetail.first_name);
+ $billingInfo.find("#billing-last-name").val(userDetail.last_name);
+ $billingInfo.find("#billing-city").val(userDetail.city);
+ $billingInfo.find("#billing-state").val(userDetail.state);
+ $billingInfo.find("#billing-country").val(userDetail.country);
+
+ $shippingAddress.find("#shipping-first-name").val(userDetail.first_name);
+ $shippingAddress.find("#shipping-last-name").val(userDetail.last_name);
+ $shippingAddress.find("#shipping-city").val(userDetail.city);
+ $shippingAddress.find("#shipping-state").val(userDetail.state);
+ $shippingAddress.find("#shipping-country").val(userDetail.country);
+ }
+ }
+
+ function afterShow(data) {
+ }
+
+ function next(e) {
+ e.preventDefault();
+
+ // validation
+ var billing_first_name = $billingInfo.find("#billing-first-name").val();
+ var billing_last_name = $billingInfo.find("#billing-last-name").val();
+ var billing_address1 = $billingInfo.find("#billing-address1").val();
+ var billing_address2 = $billingInfo.find("#billing-address2").val();
+ var billing_city = $billingInfo.find("#billing-city").val();
+ var billing_state = $billingInfo.find("#billing-state").val();
+ var billing_zip = $billingInfo.find("#billing-zip").val();
+ var billing_country = $billingInfo.find("#billing-country").val();
+
+ if (!billing_first_name) {
+ $billingInfo.find('#divBillingFirstName .error-text').remove();
+ $billingInfo.find('#divBillingFirstName').addClass("error");
+ $billingInfo.find('#billing-first-name').after("");
+
+ return false;
+ }
+ else {
+ $billingInfo.find('#divBillingFirstName').removeClass("error");
+ }
+
+ if (!billing_last_name) {
+ $billingInfo.find('#divBillingLastName .error-text').remove();
+ $billingInfo.find('#divBillingLastName').addClass("error");
+ $billingInfo.find('#billing-last-name').after("");
+
+ return false;
+ }
+ else {
+ $billingInfo.find('#divBillingLastName').removeClass("error");
+ }
+
+ if (!billing_address1) {
+ $billingInfo.find('#divBillingAddress1 .error-text').remove();
+ $billingInfo.find('#divBillingAddress1').addClass("error");
+ $billingInfo.find('#billing-address1').after("");
+
+ return false;
+ }
+ else {
+ $billingInfo.find('#divBillingAddress1').removeClass("error");
+ }
+
+ if (!billing_zip) {
+ $billingInfo.find('#divBillingZip .error-text').remove();
+ $billingInfo.find('#divBillingZip').addClass("error");
+ $billingInfo.find('#billing-zip').after("");
+
+ return false;
+ }
+ else {
+ $billingInfo.find('#divBillingZip').removeClass("error");
+ }
+
+ if (!billing_state) {
+ $billingInfo.find('#divBillingState .error-text').remove();
+ $billingInfo.find('#divBillingState').addClass("error");
+ $billingInfo.find('#billing-zip').after("");
+
+ return false;
+ }
+ else {
+ $billingInfo.find('#divBillingState').removeClass("error");
+ }
+
+ if (!billing_city) {
+ $billingInfo.find('#divBillingCity .error-text').remove();
+ $billingInfo.find('#divBillingCity').addClass("error");
+ $billingInfo.find('#billing-city').after("");
+
+ return false;
+ }
+ else {
+ $billingInfo.find('#divBillingCity').removeClass("error");
+ }
+
+ if (!billing_country) {
+ $billingInfo.find('#divBillingCountry .error-text').remove();
+ $billingInfo.find('#divBillingCountry').addClass("error");
+ $billingInfo.find('#billing-country').after("");
+
+ return false;
+ }
+ else {
+ $billingInfo.find('#divBillingCountry').removeClass("error");
+ }
+
+ shipping_as_billing = $shippingAsBilling.is(":checked");
+ var shipping_first_name, shipping_last_name, shipping_address1, shipping_address2;
+ var shipping_city, shipping_state, shipping_zip, shipping_country;
+
+ if (!shipping_as_billing) {
+ shipping_first_name = $shippingAddress.find("#shipping-first-name").val();
+ shipping_last_name = $shippingAddress.find("#shipping-last-name").val();
+ shipping_address1 = $shippingAddress.find("#shipping-address1").val();
+ shipping_address2 = $shippingAddress.find("#shipping-address2").val();
+ shipping_city = $shippingAddress.find("#shipping-city").val();
+ shipping_state = $shippingAddress.find("#shipping-state").val();
+ shipping_zip = $shippingAddress.find("#shipping-zip").val();
+ shipping_country = $shippingAddress.find("#shipping-country").val();
+
+ if (!shipping_first_name) {
+ $shippingAddress.find('#divShippingFirstName .error-text').remove();
+ $shippingAddress.find('#divShippingFirstName').addClass("error");
+ $shippingAddress.find('#shipping-first-name').after("");
+
+ return false;
+ }
+ else {
+ $shippingInfo.find('#divShippingFirstName').removeClass("error");
+ }
+
+ if (!shipping_last_name) {
+ $shippingAddress.find('#divShippingLastName .error-text').remove();
+ $shippingAddress.find('#divShippingLastName').addClass("error");
+ $shippingAddress.find('#shipping-last-name').after("");
+
+ return false;
+ }
+ else {
+ $shippingInfo.find('#divShippingLastName').removeClass("error");
+ }
+
+ if (!shipping_address1) {
+ $shippingAddress.find('#divShippingAddress1 .error-text').remove();
+ $shippingAddress.find('#divShippingAddress1').addClass("error");
+ $shippingAddress.find('#shipping-address1').after("");
+
+ return false;
+ }
+ else {
+ $shippingInfo.find('#divShippingAddress1').removeClass("error");
+ }
+
+ if (!shipping_zip) {
+ $shippingAddress.find('#divShippingZip .error-text').remove();
+ $shippingAddress.find('#divShippingZip').addClass("error");
+ $shippingAddress.find('#shipping-zip').after("");
+
+ return false;
+ }
+ else {
+ $shippingInfo.find('#divShippingZip').removeClass("error");
+ }
+
+ if (!shipping_state) {
+ $shippingAddress.find('#divShippingState .error-text').remove();
+ $shippingAddress.find('#divShippingState').addClass("error");
+ $shippingAddress.find('#shipping-zip').after("");
+
+ return false;
+ }
+ else {
+ $shippingInfo.find('#divShippingState').removeClass("error");
+ }
+
+ if (!shipping_city) {
+ $shippingAddress.find('#divShippingCity .error-text').remove();
+ $shippingAddress.find('#divShippingCity').addClass("error");
+ $shippingAddress.find('#shipping-city').after("");
+
+ return false;
+ }
+ else {
+ $shippingInfo.find('#divShippingCity').removeClass("error");
+ }
+
+ if (!shipping_country) {
+ $shippingAddress.find('#divShippingCountry .error-text').remove();
+ $shippingAddress.find('#divShippingCountry').addClass("error");
+ $shippingAddress.find('#shipping-country').after("");
+
+ return false;
+ }
+ else {
+ $shippingAddress.find('#divShippingCountry').removeClass("error");
+ }
+ }
+
+ var card_name = $paymentMethod.find("#card-name").val();
+ var card_number = $paymentMethod.find("#card-number").val();
+ var card_year = $paymentMethod.find("#card_expire-date_3i").val();
+ var card_month = $paymentMethod.find("#card_expire-date_2i").val();
+ var card_verify = $paymentMethod.find("#card-verify").val();
+
+ if (!card_name) {
+ $paymentMethod.find('#divCardName .error-text').remove();
+ $paymentMethod.find('#divCardName').addClass("error");
+ $paymentMethod.find('#card-name').after("");
+
+ return false;
+ }
+ else {
+ $paymentMethod.find('#divCardName').removeClass("error");
+ }
+
+ if (!card_number) {
+ $paymentMethod.find('#divCardNumber .error-text').remove();
+ $paymentMethod.find('#divCardNumber').addClass("error");
+ $paymentMethod.find('#card-number').after("");
+
+ return false;
+ }
+ else {
+ $paymentMethod.find('#divCardNumber').removeClass("error");
+ }
+
+ if (!card_verify) {
+ $paymentMethod.find('#divCardVerify .error-text').remove();
+ $paymentMethod.find('#divCardVerify').addClass("error");
+ $paymentMethod.find('#card_verify').after("- Card Verification Value is required
");
+
+ return false;
+ }
+ else {
+ $paymentMethod.find('#divCardVerify').removeClass("error");
+ }
+
+ billing_info = {};
+ shipping_info = {};
+ billing_info.first_name = billing_first_name;
+ billing_info.last_name = billing_last_name;
+ billing_info.address1 = billing_address1;
+ billing_info.address2 = billing_address2;
+ billing_info.city = billing_city;
+ billing_info.state = billing_state;
+ billing_info.country = billing_country;
+ billing_info.zip = billing_zip;
+ billing_info.number = card_number;
+ billing_info.month = card_month;
+ billing_info.year = card_year;
+ billing_info.verification_value = card_verify;
+
+ if (shipping_as_billing) {
+ shipping_info = billing_info;
+ delete shipping_info.number;
+ delete shipping_info.month;
+ delete shipping_info.year;
+ delete shipping_info.verification_value;
+ }
+ else {
+ shipping_info.first_name = shipping_first_name;
+ shipping_info.last_name = shipping_last_name;
+ shipping_info.address1 = shipping_address1;
+ shipping_info.address2 = shipping_address2;
+ shipping_info.city = shipping_city;
+ shipping_info.state = shipping_state;
+ shipping_info.country = shipping_country;
+ shipping_info.zip = shipping_zip;
+ }
+
+ $paymentInfoPanel.find("#payment-info-next").addClass("disabled");
+ $paymentInfoPanel.find("#payment-info-next").off("click");
+
+ if (userDetail.has_recurly_account) {
+ rest.updateBillingInfo(billing_info)
+ .done(function() {
+ })
+ .fail(errorHandling);
+ }
+ else {
+ rest.createRecurlyAccount({billing_info: billing_info})
+ .done(function() {
+ })
+ .fail(errorHandling);
+ }
+ }
+
+ function errorHandling(xhr, ajaxOptions, thrownError) {
+ $.each(xhr.responseJSON.errors, function(key, error) {
+ if (key == 'number') {
+ $paymentMethod.find('#divCardNumber .error-text').remove();
+ $paymentMethod.find('#divCardNumber').addClass("error");
+ $paymentMethod.find('#card-number').after("");
+ }
+ else if (key == 'verification_value') {
+ $paymentMethod.find('#divCardVerify .error-text').remove();
+ $paymentMethod.find('#divCardVerify').addClass("error");
+ $paymentMethod.find('#card-verify').after("");
+ }
+ });
+
+ $paymentInfoPanel.find("#payment-info-next").removeClass("disabled");
+ $paymentInfoPanel.find("#payment-info-next").on("click", next);
+// moveToOrder();
+ }
+
+ function beforeShowOrder() {
+ step = 3;
+ renderNavigation();
+ populateOrderPage();
+ }
+
+ function clearOrderPage() {
+ $orderContent.empty();
+ }
+
+ function populateOrderPage() {
+ clearOrderPage();
+
+ rest.getShoppingCarts()
+ .done(renderOrderPage)
+ .fail(app.ajaxError);
+ }
+
+ function renderOrderPage(carts) {
+ var data = {};
+
+ var sub_total = 0;
+ $.each(carts, function(index, cart) {
+ sub_total += parseFloat(cart.product_info.price) * parseFloat(cart.quantity);
+ });
+ data.sub_total = sub_total.toFixed(2);
+ data.taxes = 12.01;
+
+ data.carts = carts;
+ data.billing_info = billing_info;
+ data.shipping_info = shipping_info;
+ data.shipping_as_billing = shipping_as_billing;
+ var orderContentHtml = $(
+ context._.template(
+ $('#template-order-content').html(),
+ data,
+ {variable: 'data'}
+ )
+ );
+
+ $orderContent.append(orderContentHtml);
+
+ $orderPanel.find(".change-payment-info").on('click', moveToPaymentInfo);
+ $orderContent.find(".place-order").on('click', placeOrder);
+ }
+
+ function moveToOrder() {
+ $paymentInfoPanel.addClass("hidden");
+ $orderPanel.removeClass("hidden");
+ beforeShowOrder();
+ }
+
+ function moveToPaymentInfo(e) {
+ e.preventDefault();
+ $paymentInfoPanel.removeClass("hidden");
+ $orderPanel.addClass("hidden");
+ beforeShowPaymentInfo();
+ }
+
+ function toggleShippingAsBilling(e) {
+ e.preventDefault();
+
+ var shipping_as_billing = $(e.target).is(':checked');
+
+ if (!shipping_as_billing) {
+ $shippingAddress.removeClass("hidden");
+ }
+ else {
+ $shippingAddress.addClass("hidden");
+ }
+ }
+
+ function placeOrder(e) {
+ e.preventDefault();
+ }
+
+ function events() {
+ $paymentInfoPanel.find("#payment-info-next").on('click', next);
+ $shippingAsBilling.on('ifChanged', toggleShippingAsBilling);
+ }
+
+ function reset() {
+ }
+
+ function renderNavigation() {
+ $navigation.html("");
+ var navigationHtml = $(
+ context._.template(
+ $('#template-checkout-navigation').html(),
+ {current: step},
+ {variable: 'data'}
+ )
+ );
+
+ $navigation.append(navigationHtml);
+ }
+
+ function initializeControls() {
+ $("form.payment-info").iCheck({
+ checkboxClass: 'icheckbox_minimal',
+ radioClass: 'iradio_minimal',
+ inheritClass: true
+ });
+ }
+
+ function initialize() {
+ var screenBindings = {
+ 'beforeShow': beforeShow,
+ 'afterShow': afterShow
+ };
+ app.bindScreen('order', screenBindings);
+
+ $screen = $("#orderScreen");
+ $paymentInfoPanel = $screen.find(".checkout-payment-info");
+ $orderPanel = $screen.find(".order-panel");
+ $navigation = $screen.find(".checkout-navigation-bar");
+ $billingInfo = $paymentInfoPanel.find(".billing-address");
+ $shippingInfo = $paymentInfoPanel.find(".shipping-address");
+ $paymentMethod = $paymentInfoPanel.find(".payment-method");
+ $shippingAddress = $paymentInfoPanel.find(".shipping-address-detail");
+ $shippingAsBilling = $paymentInfoPanel.find("#shipping-as-billing");
+ $orderContent = $orderPanel.find(".order-content");
+
+ if($screen.length == 0) throw "$screen must be specified";
+ if($navigation.length == 0) throw "$navigation must be specified";
+
+ initializeControls();
+
+ events();
+ }
+
+ this.initialize = initialize;
+
+ return this;
+ }
+})(window,jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/shopping_cart.js b/web/app/assets/javascripts/shopping_cart.js
new file mode 100644
index 000000000..ac02b8b39
--- /dev/null
+++ b/web/app/assets/javascripts/shopping_cart.js
@@ -0,0 +1,112 @@
+(function(context,$) {
+
+ "use strict";
+ context.JK = context.JK || {};
+ context.JK.ShoppingCartScreen = function(app) {
+
+ var logger = context.JK.logger;
+
+ var $screen = null;
+ var $content = null;
+
+ function beforeShow(data) {
+ loadShoppingCarts();
+ }
+
+ function afterShow(data) {
+ }
+
+ function events() {
+ $screen.find("a.remove-cart").on('click', removeCart);
+ $screen.find("a.proceed-checkout").on('click', proceedCheckout);
+ }
+
+ function proceedCheckout(e) {
+ e.preventDefault();
+
+ if (!context.JK.currentUserId) {
+ window.location = '/client#/signin';
+ }
+ else {
+ window.location = '/client#/order';
+ }
+ }
+
+ function removeCart(e) {
+ e.preventDefault();
+
+ var options = {};
+ options.id = $(e.target).attr("cart-id");
+
+ rest.removeShoppingCart(options)
+ .done(loadShoppingCarts)
+ .fail(app.ajaxError);
+ }
+
+ function clearContent() {
+ $content.empty();
+ }
+
+ function loadShoppingCarts() {
+ clearContent();
+
+ rest.getShoppingCarts()
+ .done(renderShoppingCarts)
+ .fail(app.ajaxError);
+ }
+
+ function renderShoppingCarts(carts) {
+ var data = {};
+ var latest_cart = carts[carts.length - 1];
+
+ var $latestCartHtml = "";
+
+ if (latest_cart) {
+ $latestCartHtml = $(
+ context._.template(
+ $('#template-shopping-cart-header').html(),
+ latest_cart,
+ {variable: 'data'}
+ )
+ );
+ }
+
+ var sub_total = 0;
+ $.each(carts, function(index, cart) {
+ sub_total += parseFloat(cart.product_info.price) * parseFloat(cart.quantity);
+ });
+ data.sub_total = sub_total.toFixed(2);
+
+ data.carts = carts;
+ var $cartsHtml = $(
+ context._.template(
+ $('#template-shopping-cart-body').html(),
+ data,
+ {variable: 'data'}
+ )
+ );
+
+ $content.append($latestCartHtml).append($cartsHtml);
+
+ events();
+ }
+
+ function initialize() {
+ var screenBindings = {
+ 'beforeShow': beforeShow,
+ 'afterShow': afterShow
+ };
+ app.bindScreen('shoppingCart', screenBindings);
+
+ $screen = $("#shoppingCartScreen");
+ $content = $screen.find(".shopping-cart-content");
+
+ if($screen.length == 0) throw "$screen must be specified";
+ if($content.length == 0) throw "$content must be specified";
+ }
+
+ this.initialize = initialize;
+
+ return this;
+ }
+})(window,jQuery);
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/client/checkout.css.scss b/web/app/assets/stylesheets/client/checkout.css.scss
new file mode 100644
index 000000000..7cb65eac0
--- /dev/null
+++ b/web/app/assets/stylesheets/client/checkout.css.scss
@@ -0,0 +1,252 @@
+.checkout-navigation {
+ padding: 20px 0px;
+ .nav-signin, .nav-payment-info, .nav-place-order {
+ width: 30%;
+ float: left;
+ }
+
+ .nav-signin {
+ margin-left: 5%;
+ }
+
+ .nav-place-order {
+ margin-right: 5%;
+ }
+
+ .nav-text {
+ font-size: 17px;
+ float: left;
+ }
+
+ .nav-text.selected {
+ font-weight: bold;
+ }
+
+ .nav-arrow {
+ float: left;
+ margin-left: 30px;
+ }
+}
+
+.checkout-signin, .checkout-payment-info, .checkout-place-order {
+ padding: 30px;
+
+ .signin-form {
+ padding: 10px;
+
+ strong {
+ font-weight: bold;
+ }
+
+ label {
+ display: inline;
+ }
+
+ .signin-password {
+ margin-left: 33px;
+ }
+
+ .login-error {
+ background-color: #330000;
+ border: 1px solid #990000;
+ padding:4px;
+
+ div.actions {
+ margin-top:10px;
+ }
+ }
+
+ .login-error-msg {
+ display:none;
+ margin-top:10px;
+ text-align:center;
+ color:#F00;
+ font-size:11px;
+ }
+
+ .login-error .login-error-msg {
+ display:block;
+ }
+ }
+
+ form.payment-info {
+ width: 100%;
+
+ input[type="text"] {
+ width: 90%;
+ }
+
+ .billing-address {
+ float: left;
+ width: 50%;
+
+ h2.billing-caption {
+ margin: 20px 5px;
+ font-size: 16px;
+ }
+
+ .billing-label {
+ padding-top: 8px;
+ width: 30%;
+ float: left;
+ text-align: right;
+ margin-right: 5px;
+ }
+
+ .billing-value {
+ width: 65%;
+ text-align: left;
+ float: left;
+ }
+ }
+
+ .payment-method {
+ float: left;
+ width: 50%;
+
+ h2.payment-method-caption {
+ margin: 20px 5px;
+ font-size: 16px;
+ }
+
+ .card-label {
+ padding-top: 8px;
+ width: 35%;
+ float: left;
+ text-align: right;
+ margin-right: 5px;
+ }
+
+ .card-value {
+ width: 60%;
+ text-align: left;
+ float: left;
+ }
+
+ .save-card-checkbox {
+ float:left;
+ display:block;
+ margin-right:5px;
+ }
+ }
+
+ .shipping-address {
+ float: left;
+ width: 50%;
+
+ h2.shipping-address-label {
+ margin: 20px 5px;
+ font-size: 16px;
+ }
+
+ .shipping-as-billing {
+ float:left;
+ display:block;
+ margin-right:5px;
+ }
+
+ .divBillingHelper {
+ padding-top: 2px;
+ }
+
+ .shipping-label {
+ padding-top: 8px;
+ width: 30%;
+ float: left;
+ text-align: right;
+ margin-right: 5px;
+ }
+
+ .shipping-value {
+ width: 65%;
+ text-align: left;
+ float: left;
+ }
+ }
+ }
+}
+
+.order-panel {
+ padding: 30px;
+
+ .order-header {
+ h2 {
+ font-size: 16px;
+ }
+ }
+
+ .order-content {
+ margin-top: 20px;
+ }
+
+ .order-left-page {
+ float: left;
+ width: 60%;
+
+ .payment-info-page {
+ padding: 5px;
+
+ .info-caption-link {
+ .caption-text {
+ float: left;
+ }
+ .caption-link {
+ float: left;
+ margin-left: 5px;
+ }
+ }
+
+ .address-info {
+ width: 50%;
+ float: left;
+ }
+
+ .payment-method-info {
+ width: 50%;
+ float: left;
+ }
+ }
+ .order-items-page {
+ padding: 5px;
+
+ .cart-item-caption {
+ width: 50%;
+ text-align: left;
+ float: left;
+ }
+
+ .cart-item-caption#header {
+ font-weight: bold;
+ }
+
+ .cart-item-price {
+ width: 25%;
+ text-align: right;
+ float: left;
+ }
+
+ .cart-item-quantity {
+ width: 25%;
+ text-align: right;
+ float: left;
+ }
+
+ .cart-items {
+ margin-top: 10px;
+ }
+
+ .cart-item {
+ margin-top: 10px;
+ }
+ }
+ }
+ .order-right-page {
+ float: right;
+ width: 35%;
+ text-align: center;
+
+ .order-total {
+ color: #ed3618;
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css
index 75d262cbe..c5dd4d21c 100644
--- a/web/app/assets/stylesheets/client/client.css
+++ b/web/app/assets/stylesheets/client/client.css
@@ -48,6 +48,9 @@
*= require ./terms
*= require ./createSession
*= require ./feed
+ *= require ./jamtrack
+ *= require ./shoppingCart
+ *= require ./checkout
*= require ./genreSelector
*= require ./sessionList
*= require ./searchResults
diff --git a/web/app/assets/stylesheets/client/header.css.scss b/web/app/assets/stylesheets/client/header.css.scss
index 2812d087f..8bdedd4ea 100644
--- a/web/app/assets/stylesheets/client/header.css.scss
+++ b/web/app/assets/stylesheets/client/header.css.scss
@@ -7,6 +7,15 @@
z-index:5;
}
+.header-shopping-cart {
+ float: right;
+ margin-right: 20px;
+
+ img {
+ width: 60px;
+ }
+}
+
div[layout="header"] h1 {
cursor:pointer;
width: 247px;
diff --git a/web/app/assets/stylesheets/client/home.css.scss b/web/app/assets/stylesheets/client/home.css.scss
index 953d7f87e..d9b7d9d45 100644
--- a/web/app/assets/stylesheets/client/home.css.scss
+++ b/web/app/assets/stylesheets/client/home.css.scss
@@ -40,6 +40,9 @@
.homecard.musicians {
background-image: url(/assets/content/bkg_home_musicians.jpg);
}
+.homecard.jamtrack {
+ background-image: url(/assets/content/bkg_home_jamtracks.jpg);
+}
.homebox-info {
position: absolute;
@@ -93,6 +96,9 @@
.homecard.musicians.hover {
background-image: url(/assets/content/bkg_home_musicians_x.jpg);
}
+.homecard.jamtrack.hover {
+ background-image: url(/assets/content/bkg_home_jamtracks_x.jpg);
+}
diff --git a/web/app/assets/stylesheets/client/jamtrack.css.scss b/web/app/assets/stylesheets/client/jamtrack.css.scss
new file mode 100644
index 000000000..7d69b1b6f
--- /dev/null
+++ b/web/app/assets/stylesheets/client/jamtrack.css.scss
@@ -0,0 +1,116 @@
+#jamtrackScreen {
+ a.jamtrack_help {
+ color: #fff;
+ text-decoration: none;
+ margin: 4px 0px 0px 60px;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ .jamtrack-content {
+ text-align: center;
+ }
+
+ .no-jamtracks-msg {
+ margin-top: 10px;
+ }
+
+ .jamtrack-record {
+ border-bottom: 1px solid black;
+ text-align: left;
+ }
+
+ .jamtrack-detail {
+ float: left;
+ width: 50%;
+ padding: 10px 0px;
+
+ .detail-label {
+ width: 40%;
+ float: left;
+ margin-top: 5px;
+ }
+
+ .detail-value {
+ width: 50%;
+ float: left;
+ margin-top: 5px;
+ }
+
+ .copyright-value {
+ width: 40%;
+ float: left;
+ margin-top: 5px;
+ }
+
+ .detail-arrow {
+ float: left;
+ margin-left: 10px;
+ }
+
+ .jamtrack-description {
+ display: none;
+ }
+
+ .jamtrack-detail-btn {
+ cursor: pointer;
+ margin-top: 5px;
+ margin-right: 5px;
+ padding-top: 5px;
+ }
+ }
+
+ .jamtrack-tracks {
+ float: left;
+ width: 25%;
+ padding: 10px 0px;
+
+ .tracks-caption {
+ margin-top: 5px;
+ margin-bottom: 10px;
+ }
+
+ .track-instrument {
+ margin-top: 5px;
+ }
+
+ .instrument-image {
+ float: left;
+ }
+
+ .instrument-desc {
+ margin-top: 6px;
+ float: left;
+ margin-left: 10px;
+ }
+ }
+
+ .jamtrack-action {
+ float: left;
+ width: 25%;
+ padding: 10px 0px;
+ text-align: center;
+
+ .play-button {
+ margin-top: 5px;
+ }
+
+ .jamtrack-price {
+ margin-top: 5px;
+ font-size: 20px;
+ }
+
+ .jamtrack-add-cart, .jamtrack-add-cart-disabled {
+ margin: 8px 0px;
+ }
+
+ .jamtrack-license {
+ margin-left: 20%;
+ margin-right: 20%;
+ font-size: 13px;
+ width: 60%;
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/client/shoppingCart.css.scss b/web/app/assets/stylesheets/client/shoppingCart.css.scss
new file mode 100644
index 000000000..63f333543
--- /dev/null
+++ b/web/app/assets/stylesheets/client/shoppingCart.css.scss
@@ -0,0 +1,78 @@
+#shoppingCartScreen {
+
+ .content-body {
+ padding: 50px 20px 20px 20px;
+
+ .checkout-image {
+ width: 10%;
+ float: left;
+ }
+
+ .checkout-desc {
+ width: 90%;
+ float: left;
+
+ div {
+ margin-bottom: 7px;
+ }
+
+ div#note {
+ font-style: italic;
+ font-size: 13px;
+ }
+ }
+
+ .cart-item-caption {
+ width: 50%;
+ text-align: left;
+ float: left;
+ }
+
+ .cart-item-caption#header {
+ font-weight: bold;
+ }
+
+ .cart-item-price {
+ width: 15%;
+ text-align: right;
+ float: left;
+ }
+
+ .cart-item-quantity {
+ width: 15%;
+ text-align: right;
+ float: left;
+ }
+
+ .cart-item-actions {
+ width: 20%;
+ text-align: center;
+ float: left;
+ }
+
+ .cart-items {
+ width: 100%;
+ height: 300px;
+ overflow: auto;
+ margin-top: 30px;
+ }
+
+ .cart-item {
+ margin-top: 10px;
+ }
+
+ .shopping-sub-total {
+ width: 65%;
+ float: left;
+ text-align: right;
+ margin-bottom: 20px;
+ font-weight: bold;
+ font-size: 17px;
+ }
+
+ .no-cart-items {
+ margin-top: 30px;
+ text-align: center;
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/dialogs/jamtrackAvailability.css.scss b/web/app/assets/stylesheets/dialogs/jamtrackAvailability.css.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/web/app/controllers/api_jamtracks_controller.rb b/web/app/controllers/api_jamtracks_controller.rb
new file mode 100644
index 000000000..8bbc2c179
--- /dev/null
+++ b/web/app/controllers/api_jamtracks_controller.rb
@@ -0,0 +1,15 @@
+class ApiJamtracksController < ApiController
+
+ # have to be signed in currently to see this screen
+ before_filter :api_signed_in_user
+
+ respond_to :json
+
+ def index
+ data = JamTrack.index current_user, params
+
+ @jamtracks = data[0]
+ @next = data[1]
+ end
+
+end
diff --git a/web/app/controllers/api_recurly_controller.rb b/web/app/controllers/api_recurly_controller.rb
new file mode 100644
index 000000000..4c841320d
--- /dev/null
+++ b/web/app/controllers/api_recurly_controller.rb
@@ -0,0 +1,155 @@
+class ApiRecurlyController < ApiController
+
+ before_filter :api_signed_in_user
+ respond_to :json
+
+ # create Recurly account
+ def create_account
+ logger.debug(params[:billing_info])
+ if current_user.recurly_code.nil?
+ @account = Recurly::Account.create(
+ account_code: current_user.id,
+ email: current_user.email,
+ first_name: current_user.first_name,
+ last_name: current_user.last_name,
+ address: {
+ city: current_user.city,
+ state: current_user.state,
+ country: current_user.country
+ }
+ )
+ else
+ @account = Recurly::Account.find(current_user.recurly_code)
+ end
+
+ if @account.errors.any?
+ response.status = 404
+ else
+ current_user.recurly_code = @account.account_code
+ current_user.save
+
+ @account.billing_info = params[:billing_info]
+ @account.billing_info.save
+
+ logger.debug @account
+ end
+ respond_with @account
+
+ rescue Recurly::Error, NoMethodError => e
+ render :json => { :message => e.inspect }, :status => 404
+ end
+
+ # get Recurly account
+ def get_account
+ @account = Recurly::Account.find(current_user.reculry_code)
+ respond_with @account
+ rescue Recurly::Error, NoMethodError => e
+ render :json => { message: ValidationMessages::RECURLY_ERROR}, :status => 404
+ end
+
+ # update Recurly account
+ def update_account
+ if current_user.recurly_code.nil?
+ @account = Recurly::Account.create(
+ account_code: current_user.id,
+ email: current_user.email,
+ first_name: current_user.first_name,
+ last_name: current_user.last_name,
+ address: {
+ city: current_user.city,
+ state: current_user.state,
+ country: current_user.country
+ }
+ )
+ else
+ @account = Recurly::Account.get(current_user.recurly_code)
+ end
+
+ @account.first_name = current_user.first_name
+ @account.last_name = current_user.last_name
+ @account.email = current_user.email
+ @account.update
+
+ if @account.errors.any?
+ response.status = 404
+ else
+ current_user.recurly_code = @account.account_code
+ current_user.save
+ end
+ respond_with @account
+
+ rescue Recurly::Error, NoMethodError => e
+ render :json => { message: ValidationMessages::RECURLY_ERROR}, :status => 404
+ end
+
+ # get subscription
+ def get_subscription
+ @account = Recurly::Acount.find(current_user.reculry_code)
+ respond_with @account.subscriptions.last
+ rescue Recurly::Error, NoMethodError => e
+ render :json => { message: ValidationMessages::RECURLY_GET_ACCOUNT_ERROR}, :status => 404
+ end
+
+ # create subscription
+ def create_subscription
+ end
+
+ # get Billing Information
+ def billing_info
+ if current_user.recurly_code.nil?
+ render :json => { message: ValidationMessages::RECURLY_ACCOUNT_ERROR }, :status => 404 and return
+ else
+ @account = Recurly::Account.find(current_user.recurly_code)
+ logger.debug @account
+ respond_with @account.billing_info
+ end
+ rescue Recurly::Error, NoMethodError => e
+ render :json => { message: ValidationMessages::RECURLY_ERROR}, :status => 404
+ end
+
+ # update Billing Information
+ def update_billing_info
+ if current_user.recurly_code.nil?
+ render :json => { message: ValidationMessages::RECURLY_ACCOUNT_ERROR }, :status => 404 and return
+ else
+ if params[:first_name].blank? or params[:last_name].blank? or params[:number].blank? or params[:year].blank? or params[:month].blank? or params[:verification_value].blank?
+ render :json => { message: ValidationMessages::RECURLY_PARAMETER_ERROR }, :status => 404 and return
+ end
+ @account = Recurly::Acount.find(current_user.reculry_code)
+ @account.billing_info = params
+ @account.billing_info.save
+
+ if @account.erros.any?
+ response.status = :unprocessable_entity
+ end
+
+ respond_with @account
+ end
+ rescue Recurly::Error, NoMethodError => e
+ render :json => { message: ValidationMessages::RECURLY_ERROR}, :status => 404
+ end
+
+ def place_order
+ if current_user.recurly_code.nil?
+ render :json => { message: ValidationMessages::RECURLY_ACCOUNT_ERROR }, :status => 404 and return
+ else
+ if params[:first_name].blank? or params[:last_name].blank? or params[:number].blank? or params[:year].blank? or params[:month].blank? or params[:verification_value].blank?
+ render :json => { message: ValidationMessages::RECURLY_PARAMETER_ERROR }, :status => 404 and return
+ end
+ @account = Recurly::Account.find(current_user.recurly_code)
+ @account.billing_info = params
+ @account.billing_info.save
+
+ # create subscription.
+
+ if @account.erros.any?
+ response.status = :unprocessable_entity
+ end
+
+ respond_with @account
+ end
+ rescue Recurly::Error, NoMethodError => e
+ render :json => { message: ValidationMessages::RECURLY_ERROR}, :status => 404
+ end
+
+end
\ No newline at end of file
diff --git a/web/app/controllers/api_shopping_carts_controller.rb b/web/app/controllers/api_shopping_carts_controller.rb
new file mode 100644
index 000000000..d1109e821
--- /dev/null
+++ b/web/app/controllers/api_shopping_carts_controller.rb
@@ -0,0 +1,53 @@
+class ApiShoppingCartsController < ApiController
+
+ before_filter :api_signed_in_user
+
+ respond_to :json
+
+ def index
+ @carts = current_user.shopping_carts
+ end
+
+ def add_jamtrack
+ jam_track = JamTrack.find_by_id(params[:id])
+
+ # verify JamTrack exists
+ if jam_track.nil?
+ raise StateError, "Invalid JamTrack."
+ end
+
+ @cart = ShoppingCart.create current_user, jam_track
+
+ if @cart.errors.any?
+ response.status = :unprocessable_entity
+ respond_with @cart
+ else
+ respond_with @cart, responder: ApiResponder, :statue => 201
+ end
+ end
+
+ def update_cart
+ @cart = ShoppingCart.find_by_id params[:id]
+
+ #verify Cart exists
+ raise StateError, "Invalid Cart." if @cart.nil?
+
+ @cart.quantity = params[:quantity]
+
+ if @cart.errors.any?
+ response.statue = :unprocessable_entity
+ respond_with @cart
+ else
+ respond_with @cart, responder: ApiResponder, :status => 200
+ end
+ end
+
+ def remove_cart
+ @cart = current_user.shopping_carts.find_by_id(params[:id])
+ raise StateError, "Invalid Cart." if @cart.nil?
+
+ @cart.destroy
+ respond_with responder: ApiResponder, :status => 204
+ end
+
+end
diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb
index d9b473307..e475a4a70 100644
--- a/web/app/controllers/api_users_controller.rb
+++ b/web/app/controllers/api_users_controller.rb
@@ -1,7 +1,7 @@
require 'sanitize'
class ApiUsersController < ApiController
- before_filter :api_signed_in_user, :except => [:create, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play]
+ before_filter :api_signed_in_user, :except => [:create, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump]
before_filter :auth_user, :only => [:session_settings_show, :session_history_index, :session_user_history_index, :update, :delete,
:liking_create, :liking_destroy, # likes
:following_create, :following_show, :following_destroy, # followings
diff --git a/web/app/views/api_jamtracks/index.rabl b/web/app/views/api_jamtracks/index.rabl
new file mode 100644
index 000000000..25c3f9da5
--- /dev/null
+++ b/web/app/views/api_jamtracks/index.rabl
@@ -0,0 +1,7 @@
+node :next do |page|
+ @next
+end
+
+node :jamtracks do |page|
+ partial "api_jamtracks/show", object: @jamtracks
+end
\ No newline at end of file
diff --git a/web/app/views/api_jamtracks/show.rabl b/web/app/views/api_jamtracks/show.rabl
new file mode 100644
index 000000000..dde7bb3dd
--- /dev/null
+++ b/web/app/views/api_jamtracks/show.rabl
@@ -0,0 +1,19 @@
+object @jamtrack
+
+attributes :id, :name, :description, :recording_type, :original_artist, :songwriter, :publisher, :sales_region, :price
+
+node :genres do |item|
+ [item.genre.description] # XXX: need to return single genre; not array
+end
+
+node :added_cart do |item|
+ current_user.shopping_carts.map(&:cart_id).include? item.id
+end
+
+child(:jam_track_tracks => :tracks) {
+ attributes :id, :part, :instrument
+}
+
+child(:licensor => :licensor) {
+ attributes :id, :name
+}
\ No newline at end of file
diff --git a/web/app/views/api_shopping_carts/add_jamtrack.rabl b/web/app/views/api_shopping_carts/add_jamtrack.rabl
new file mode 100644
index 000000000..c61e7b52b
--- /dev/null
+++ b/web/app/views/api_shopping_carts/add_jamtrack.rabl
@@ -0,0 +1 @@
+extends "api_shopping_carts/show"
\ No newline at end of file
diff --git a/web/app/views/api_shopping_carts/index.rabl b/web/app/views/api_shopping_carts/index.rabl
new file mode 100644
index 000000000..bc63bab24
--- /dev/null
+++ b/web/app/views/api_shopping_carts/index.rabl
@@ -0,0 +1,3 @@
+object @carts
+
+extends "api_shopping_carts/show"
\ No newline at end of file
diff --git a/web/app/views/api_shopping_carts/show.rabl b/web/app/views/api_shopping_carts/show.rabl
new file mode 100644
index 000000000..e371ba4c9
--- /dev/null
+++ b/web/app/views/api_shopping_carts/show.rabl
@@ -0,0 +1,3 @@
+object @cart
+
+attributes :id, :quantity, :cart_type, :product_info
\ No newline at end of file
diff --git a/web/app/views/api_users/show.rabl b/web/app/views/api_users/show.rabl
index 59d72f304..aa1030f48 100644
--- a/web/app/views/api_users/show.rabl
+++ b/web/app/views/api_users/show.rabl
@@ -21,6 +21,10 @@ if @user == current_user
user.mods_json
end
+ node :has_recurly_account do
+ @user.recurly_code == @user.id
+ end
+
elsif current_user
node :is_friend do |uu|
current_user.friends?(@user)
@@ -85,4 +89,4 @@ end
node :last_jam_audio_latency do |user|
user.last_jam_audio_latency.round if user.last_jam_audio_latency
-end
\ No newline at end of file
+end
diff --git a/web/app/views/clients/_header.html.erb b/web/app/views/clients/_header.html.erb
index 5195512ff..bad8f5229 100644
--- a/web/app/views/clients/_header.html.erb
+++ b/web/app/views/clients/_header.html.erb
@@ -11,6 +11,11 @@
<%= render "users/user_dropdown" %>
+
+
+