diff --git a/admin/app/admin/affiliates.rb b/admin/app/admin/affiliates.rb index e517b3e12..0409ee36c 100644 --- a/admin/app/admin/affiliates.rb +++ b/admin/app/admin/affiliates.rb @@ -2,48 +2,47 @@ ActiveAdmin.register JamRuby::AffiliatePartner, :as => 'Affiliates' do menu :label => 'Partners', :parent => 'Affiliates' - config.sort_order = 'created_at DESC' + config.sort_order = 'referral_user_count DESC' config.batch_actions = false # config.clear_action_items! config.filters = false form :partial => 'form' + scope("Active", default: true) { |scope| scope.where('partner_user_id IS NOT NULL') } + scope("Unpaid") { |partner| partner.unpaid } + index do - column 'User' do |oo| link_to(oo.partner_user.name, "http://www.jamkazam.com/client#/profile/#{oo.partner_user.id}", {:title => oo.partner_user.name}) end - column 'Email' do |oo| oo.partner_user.email end + + # default_actions # use this for all view/edit/delete links + + column 'User' do |oo| link_to(oo.partner_user.name, admin_user_path(oo.partner_user.id), {:title => oo.partner_user.name}) end column 'Name' do |oo| oo.partner_name end - column 'Code' do |oo| oo.partner_code end + column 'Type' do |oo| oo.entity_type end + column 'Code' do |oo| oo.id end column 'Referral Count' do |oo| oo.referral_user_count end - # column 'Referrals' do |oo| link_to('View', admin_referrals_path(AffiliatePartner::PARAM_REFERRAL => oo.id)) end + column 'Earnings' do |oo| sprintf("$%.2f", oo.cumulative_earnings_in_dollars) end + column 'Amount Owed' do |oo| sprintf("$%.2f", oo.due_amount_in_cents.to_f / 100.to_f) end + column 'Pay Actions' do |oo| + link_to('Mark Paid', mark_paid_admin_affiliate_path(oo.id), :confirm => "Mark this affiliate as PAID?") if oo.unpaid + end + default_actions end + + action_item :only => [:show] do + link_to("Mark Paid", + mark_paid_admin_affiliate_path(resource.id), + :confirm => "Mark this affiliate as PAID?") if resource.unpaid + end + + member_action :mark_paid, :method => :get do + resource.mark_paid + redirect_to admin_affiliate_path(resource.id) + end + controller do - def show - redirect_to admin_referrals_path(AffiliatePartner::PARAM_REFERRAL => resource.id) - end - - def create - obj = AffiliatePartner.create_with_params(params[:jam_ruby_affiliate_partner]) - if obj.errors.present? - set_resource_ivar(obj) - render active_admin_template('new') - else - redirect_to admin_affiliates_path - end - end - - def update - obj = resource - vals = params[:jam_ruby_affiliate_partner] - obj.partner_name = vals[:partner_name] - obj.user_email = vals[:user_email] if vals[:user_email].present? - obj.save! - set_resource_ivar(obj) - render active_admin_template('show') - end - end end diff --git a/admin/app/admin/fake_purchaser.rb b/admin/app/admin/fake_purchaser.rb index 75be6c653..b8c5caa31 100644 --- a/admin/app/admin/fake_purchaser.rb +++ b/admin/app/admin/fake_purchaser.rb @@ -4,8 +4,6 @@ ActiveAdmin.register_page "Fake Purchaser" do page_action :bulk_jamtrack_purchase, :method => :post do - puts params.inspect - user_field = params[:jam_ruby_jam_track_right][:user] if user_field.blank? diff --git a/admin/app/admin/fraud_alert.rb b/admin/app/admin/fraud_alert.rb new file mode 100644 index 000000000..7c4754a83 --- /dev/null +++ b/admin/app/admin/fraud_alert.rb @@ -0,0 +1,69 @@ +ActiveAdmin.register JamRuby::FraudAlert, :as => 'Fraud Alerts' do + + menu :label => 'Fraud Alerts', :parent => 'JamTracks' + + config.sort_order = 'created_at desc' + config.batch_actions = false + + + scope("Not Whitelisted", default:true) { |scope| + scope.joins('INNER JOIN "machine_fingerprints" ON "machine_fingerprints"."id" = "fraud_alerts"."machine_fingerprint_id" LEFT OUTER JOIN "fingerprint_whitelists" ON "fingerprint_whitelists"."fingerprint" = "machine_fingerprints"."fingerprint"').where('fingerprint_whitelists IS NULL')} + + index do + default_actions + + column :machine_fingerprint + column :user + column :created_at + column :resolved + + column "" do |alert| + link_to 'Matching MAC', "fraud_alerts/#{alert.id}/same_fingerprints" + end + column "" do |alert| + link_to 'Matching MAC and IP Address', "fraud_alerts/#{alert.id}/same_fingerprints_and_ip" + end + column "" do |alert| + link_to 'Matching IP Address', "fraud_alerts/#{alert.id}/same_ip" + end + column "" do |alert| + link_to 'Resolve', "fraud_alerts/#{alert.id}/resolve" + end + column "" do |alert| + link_to 'Whitelist Similar', "fraud_alerts/#{alert.id}/whitelist" + end + end + + member_action :same_fingerprints, :method => :get do + alert = FraudAlert.find(params[:id]) + + redirect_to admin_machine_fingerprints_path("q[fingerprint_equals]" => alert.machine_fingerprint.fingerprint, commit: 'Filter', order: 'created_at_desc') + end + + member_action :same_fingerprints_and_ip, :method => :get do + alert = FraudAlert.find(params[:id]) + + redirect_to admin_machine_fingerprints_path("q[fingerprint_equals]" => alert.machine_fingerprint.fingerprint, "q[remote_ip_equals]" => alert.machine_fingerprint.remote_ip, commit: 'Filter', order: 'created_at_desc') + end + + + member_action :resolve, :method => :get do + alert = FraudAlert.find(params[:id]) + alert.resolved = true + alert.save! + + redirect_to admin_fraud_alerts_path, notice: "That fraud alert has been marked as resolved" + end + + member_action :whitelist, :method => :get do + alert = FraudAlert.find(params[:id]) + + wl = FingerprintWhitelist.new + wl.fingerprint = alert.machine_fingerprint.fingerprint + success = wl.save + + redirect_to admin_fraud_alerts_path, notice: success ? "Added #{alert.machine_fingerprint.fingerprint} to whitelist" : "Could not add #{alert.machine_fingerprint.fingerprint} to whiteliste" + end + + +end \ No newline at end of file diff --git a/admin/app/admin/jam_ruby_users.rb b/admin/app/admin/jam_ruby_users.rb index 0fe3c29d9..61a1d3db7 100644 --- a/admin/app/admin/jam_ruby_users.rb +++ b/admin/app/admin/jam_ruby_users.rb @@ -32,6 +32,7 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do row :birth_date row :gender row :email_confirmed + row :remember_token row :image do user.photo_url ? image_tag(user.photo_url) : '' end end active_admin_comments diff --git a/admin/app/admin/jam_track_right.rb b/admin/app/admin/jam_track_right.rb index 48da8231a..e2cc0bbe9 100644 --- a/admin/app/admin/jam_track_right.rb +++ b/admin/app/admin/jam_track_right.rb @@ -1,20 +1,29 @@ require 'jam_ruby/recurly_client' ActiveAdmin.register JamRuby::JamTrackRight, :as => 'JamTrackRights' do - menu :label => 'Purchased JamTracks', :parent => 'JamTracks' + menu :label => 'Purchased JamTracks', :parent => 'Purchases' - config.sort_order = 'updated_at DESC' + config.sort_order = 'created_at DESC' config.batch_actions = false #form :partial => 'form' + filter :user_id + + filter :user_id, + :label => "USER ID", :required => false, + :wrapper_html => { :style => "list-style: none" } + + + filter :jam_track + index do default_actions - column "Order" do |right| - link_to("Place", order_admin_jam_track_right_path(right)) + " | " + - link_to("Refund", refund_admin_jam_track_right_path(right)) - end + #column "Order" do |right| + #link_to("Place", order_admin_jam_track_right_path(right)) + " | " + + # link_to("Refund", refund_admin_jam_track_right_path(right)) + #end column "Last Name" do |right| right.user.last_name @@ -23,13 +32,15 @@ ActiveAdmin.register JamRuby::JamTrackRight, :as => 'JamTrackRights' do right.user.first_name end column "Jam Track" do |right| - link_to(right.jam_track.name, admin_jam_track_right_path(right.jam_track)) + link_to(right.jam_track.name, admin_jam_track_path(right.jam_track)) # right.jam_track end column "Plan Code" do |right| - right.jam_track.plan_code end + column "Redeemed" do |right| + right.redeemed ? 'Y' : 'N' + end end @@ -42,6 +53,9 @@ ActiveAdmin.register JamRuby::JamTrackRight, :as => 'JamTrackRights' do f.actions end +=begin + + member_action :order, :method => :get do right = JamTrackRight.where("id=?",params[:id]).first user = right.user @@ -84,4 +98,5 @@ ActiveAdmin.register JamRuby::JamTrackRight, :as => 'JamTrackRights' do redirect_to admin_jam_track_rights_path, notice: "Issued full refund on #{right.jam_track} for #{right.user.to_s}" end end +=end end \ No newline at end of file diff --git a/admin/app/admin/machine_extra.rb b/admin/app/admin/machine_extra.rb new file mode 100644 index 000000000..c1c5b8738 --- /dev/null +++ b/admin/app/admin/machine_extra.rb @@ -0,0 +1,8 @@ +ActiveAdmin.register JamRuby::MachineExtra, :as => 'Machine Extra' do + + menu :label => 'Machine Extra', :parent => 'JamTracks' + + config.sort_order = 'created_at desc' + config.batch_actions = false + +end \ No newline at end of file diff --git a/admin/app/admin/machine_fingerprint.rb b/admin/app/admin/machine_fingerprint.rb new file mode 100644 index 000000000..4ea9c3c7a --- /dev/null +++ b/admin/app/admin/machine_fingerprint.rb @@ -0,0 +1,22 @@ +ActiveAdmin.register JamRuby::MachineFingerprint, :as => 'Machine Fingerprints' do + + menu :label => 'Machine Fingerprints', :parent => 'JamTracks' + + config.sort_order = 'created_at desc' + config.batch_actions = false + + index do + column :user + column 'Hash' do |fp| + fp.fingerprint + end + column :remote_ip + column 'Detail' do |fp| + detail = fp.detail + if detail + detail.to_s + end + end + column :created_at + end +end \ No newline at end of file diff --git a/admin/app/admin/mix.rb b/admin/app/admin/mix.rb index ade3f0d4e..c8cc80bad 100644 --- a/admin/app/admin/mix.rb +++ b/admin/app/admin/mix.rb @@ -15,6 +15,7 @@ ActiveAdmin.register JamRuby::Mix, :as => 'Mixes' do end end + index :as => :block do |mix| div :for => mix do h3 "Mix (Users: #{mix.recording.users.map { |u| u.name }.join ','}) (When: #{mix.created_at.strftime('%b %d %Y, %H:%M')})" @@ -26,6 +27,16 @@ ActiveAdmin.register JamRuby::Mix, :as => 'Mixes' do row :created_at do |mix| mix.created_at.strftime('%b %d %Y, %H:%M') end row :s3_url do |mix| mix.sign_url end row :manifest do |mix| mix.manifest end + row :local_manifest do |mix| + div class: 'local-manifest' do + mix.local_manifest.to_json + end + end + row :download_script do |mix| + div class: 'download-script' do + mix.download_script + end + end row :completed do |mix| "#{mix.completed ? "finished" : "not finished"}" end if mix.completed row :completed_at do |mix| mix.completed_at.strftime('%b %d %Y, %H:%M') end @@ -39,6 +50,7 @@ ActiveAdmin.register JamRuby::Mix, :as => 'Mixes' do end end end + end end end diff --git a/admin/app/admin/music_sessions_comment.rb b/admin/app/admin/music_sessions_comment.rb new file mode 100644 index 000000000..5b651a11f --- /dev/null +++ b/admin/app/admin/music_sessions_comment.rb @@ -0,0 +1,13 @@ +ActiveAdmin.register JamRuby::MusicSessionComment, :as => 'Ratings' do + + config.per_page = 150 + config.clear_action_items! + config.sort_order = 'created_at_desc' + menu :parent => 'Sessions', :label => 'Ratings' + + index do + column :comment + column :user + column :created_at + end +end diff --git a/admin/app/admin/recurly_health.rb b/admin/app/admin/recurly_health.rb index 518648403..aebd21250 100644 --- a/admin/app/admin/recurly_health.rb +++ b/admin/app/admin/recurly_health.rb @@ -2,12 +2,9 @@ ActiveAdmin.register_page "Recurly Health" do menu :parent => 'Misc' content :title => "Recurly Transaction Totals" do - table_for Sale.check_integrity do + table_for Sale.check_integrity_of_jam_track_sales do column "Total", :total - column "Unknown", :not_known column "Successes", :succeeded - column "Failures", :failed - column "Refunds", :refunded column "Voids", :voided end end diff --git a/admin/app/admin/recurly_transaction_web_hook.rb b/admin/app/admin/recurly_transaction_web_hook.rb new file mode 100644 index 000000000..0c6942d82 --- /dev/null +++ b/admin/app/admin/recurly_transaction_web_hook.rb @@ -0,0 +1,40 @@ +ActiveAdmin.register JamRuby::RecurlyTransactionWebHook, :as => 'RecurlyHooks' do + + menu :label => 'Recurly Transaction Hooks', :parent => 'Purchases' + + config.sort_order = 'created_at DESC' + config.batch_actions = false + + actions :all, :except => [:destroy] + + #form :partial => 'form' + + + filter :transaction_type, :as => :select, :collection => JamRuby::RecurlyTransactionWebHook::HOOK_TYPES + filter :user_id, + :label => "USER ID", :required => false, + :wrapper_html => { :style => "list-style: none" } + + filter :invoice_id + + form :partial => 'form' + + index do + + default_actions + + column :transaction_type + column :transaction_at + column :amount_in_cents + column 'Transaction' do |hook| link_to('Go to Recurly', Rails.application.config.recurly_root_url + "/transactions/#{hook.recurly_transaction_id}") end + column 'Invoice' do |hook| link_to(hook.invoice_number, Rails.application.config.recurly_root_url + "/invoices/#{hook.invoice_number}") end + column :admin_description + column 'User' do |hook| link_to("#{hook.user.email} (#{hook.user.name})", admin_user_path(hook.user.id)) end + + #column "Order" do |right| + #link_to("Place", order_admin_jam_track_right_path(right)) + " | " + + # link_to("Refund", refund_admin_jam_track_right_path(right)) + #end + end + +end \ No newline at end of file diff --git a/admin/app/admin/session_info_comment.rb b/admin/app/admin/session_info_comment.rb new file mode 100644 index 000000000..cf8fb3c0c --- /dev/null +++ b/admin/app/admin/session_info_comment.rb @@ -0,0 +1,8 @@ +ActiveAdmin.register JamRuby::SessionInfoComment, :as => 'Comments' do + + config.per_page = 50 + config.clear_action_items! + config.sort_order = 'created_at_desc' + menu :parent => 'Sessions', :label => 'Comments' + +end diff --git a/admin/app/admin/test_jobs.rb b/admin/app/admin/test_jobs.rb new file mode 100644 index 000000000..974a0e74f --- /dev/null +++ b/admin/app/admin/test_jobs.rb @@ -0,0 +1,25 @@ +ActiveAdmin.register_page "Test Jobs" do + menu :parent => 'Misc' + + page_action :long_running, :method => :post do + + puts params.inspect + + time = params[:long_running][:time] + + Resque.enqueue(LongRunning, time) + + redirect_to admin_test_jobs_path, :notice => "Long Running job enqueued." + end + + + content do + + semantic_form_for LongRunning.new, :url => admin_test_jobs_long_running_path, :builder => ActiveAdmin::FormBuilder do |f| + f.inputs "Queue a long running job" do + f.input :time + end + f.actions + end + end +end diff --git a/admin/app/assets/stylesheets/custom.css.scss b/admin/app/assets/stylesheets/custom.css.scss index f253c6e8d..19e45fee7 100644 --- a/admin/app/assets/stylesheets/custom.css.scss +++ b/admin/app/assets/stylesheets/custom.css.scss @@ -22,4 +22,11 @@ } } } +} + +.download-script, .local-manifest { + white-space:pre-wrap; + background-color:white; + border:1px solid gray; + padding:5px; } \ No newline at end of file diff --git a/admin/app/models/cohort.rb b/admin/app/models/cohort.rb index 2b72a3936..facae599c 100644 --- a/admin/app/models/cohort.rb +++ b/admin/app/models/cohort.rb @@ -55,7 +55,8 @@ class Cohort < ActiveRecord::Base end def self.earliest_cohort - starting = User.where(admin: false).order(:created_at).first.created_at + user = User.where(admin: false).order(:created_at).first + starting = user.created_at if user starting = EARLIEST_DATE if starting.nil? || starting < EARLIEST_DATE starting # this is necessary to always return not null end diff --git a/admin/app/views/admin/affiliates/_form.html.erb b/admin/app/views/admin/affiliates/_form.html.erb index a4f14416e..b7a3b047d 100644 --- a/admin/app/views/admin/affiliates/_form.html.erb +++ b/admin/app/views/admin/affiliates/_form.html.erb @@ -1,13 +1,7 @@ <%= semantic_form_for([:admin, resource], :url => resource.new_record? ? admin_affiliates_path : "/admin/affiliates/#{resource.id}") do |f| %> <%= f.semantic_errors *f.object.errors.keys %> <%= f.inputs do %> - <%= f.input(:user_email, :input_html => {:maxlength => 255}) %> <%= f.input(:partner_name, :input_html => {:maxlength => 128}) %> - <% if resource.new_record? %> - <%= f.input(:partner_code, :input_html => {:maxlength => 128}) %> - <% else %> - <%= f.input(:partner_code, :input_html => {:maxlength => 128, :readonly => 'readonly'}) %> - <% end %> <% end %> <%= f.actions %> <% end %> diff --git a/admin/app/views/admin/recurly_hooks/_form.html.slim b/admin/app/views/admin/recurly_hooks/_form.html.slim new file mode 100644 index 000000000..3e1305e23 --- /dev/null +++ b/admin/app/views/admin/recurly_hooks/_form.html.slim @@ -0,0 +1,6 @@ += semantic_form_for([:admin, resource], :html => {:multipart => true}, :url => resource.new_record? ? admin_recurly_transaction_web_hooks_path : "#{ENV['RAILS_RELATIVE_URL_ROOT']}/admin/recurly_hooks/#{resource.id}") do |f| + = f.semantic_errors *f.object.errors.keys + = f.inputs name: 'Recurly Web Hook fields' do + = f.input :admin_description, :input_html => { :rows=>1, :maxlength=>200, }, hint: "this will display on the user's payment history page" + = f.input :jam_track, collection: JamRuby::JamTrack.all, include_blank: true, hint: "Please indicate which JamTrack this refund for, if not set" + = f.actions \ No newline at end of file diff --git a/admin/config/application.rb b/admin/config/application.rb index 102a03176..9add98380 100644 --- a/admin/config/application.rb +++ b/admin/config/application.rb @@ -83,6 +83,7 @@ module JamAdmin config.external_port = ENV['EXTERNAL_PORT'] || 3000 config.external_protocol = ENV['EXTERNAL_PROTOCOL'] || 'http://' config.external_root_url = "#{config.external_protocol}#{config.external_hostname}#{(config.external_port == 80 || config.external_port == 443) ? '' : ':' + config.external_port.to_s}" + config.recurly_root_url = 'https://jamkazam-development.recurly.com' # where is rabbitmq? config.rabbitmq_host = "localhost" @@ -116,7 +117,7 @@ module JamAdmin config.email_smtp_domain = 'www.jamkazam.com' config.email_smtp_authentication = :plain config.email_smtp_user_name = 'jamkazam' - config.email_smtp_password = 'jamjamblueberryjam' + config.email_smtp_password = 'snorkeltoesniffyfarce1' config.email_smtp_starttls_auto = true config.facebook_app_id = ENV['FACEBOOK_APP_ID'] || '468555793186398' diff --git a/admin/config/environments/development.rb b/admin/config/environments/development.rb index ffd6b0e8f..b449898e8 100644 --- a/admin/config/environments/development.rb +++ b/admin/config/environments/development.rb @@ -43,4 +43,7 @@ JamAdmin::Application.configure do # Show the logging configuration on STDOUT config.show_log_configuration = true + + config.email_generic_from = 'nobody-dev@jamkazam.com' + config.email_alerts_alias = 'alerts-dev@jamkazam.com' end diff --git a/admin/config/initializers/jam_track_tracks.rb b/admin/config/initializers/jam_track_tracks.rb index b05126d52..404be22c6 100644 --- a/admin/config/initializers/jam_track_tracks.rb +++ b/admin/config/initializers/jam_track_tracks.rb @@ -2,8 +2,6 @@ class JamRuby::JamTrackTrack # add a custom validation - attr_accessor :preview_generate_error - validate :preview def preview @@ -52,87 +50,4 @@ class JamRuby::JamTrackTrack end end - def generate_preview - - begin - Dir.mktmpdir do |tmp_dir| - - input = File.join(tmp_dir, 'in.ogg') - output = File.join(tmp_dir, 'out.ogg') - output_mp3 = File.join(tmp_dir, 'out.mp3') - - start = self.preview_start_time.to_f / 1000 - stop = start + 20 - - raise 'no track' unless self["url_44"] - - s3_manager.download(self.url_by_sample_rate(44), input) - - command = "sox \"#{input}\" \"#{output}\" trim #{sprintf("%.3f", start)} =#{sprintf("%.3f", stop)}" - - @@log.debug("trimming using: " + command) - - sox_output = `#{command}` - - result_code = $?.to_i - - if result_code != 0 - @@log.debug("fail #{result_code}") - @preview_generate_error = "unable to execute cut command #{sox_output}" - else - # now create mp3 off of ogg preview - - convert_mp3_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{output}\" -ab 192k \"#{output_mp3}\"" - - @@log.debug("converting to mp3 using: " + convert_mp3_cmd) - - convert_output = `#{convert_mp3_cmd}` - - result_code = $?.to_i - - if result_code != 0 - @@log.debug("fail #{result_code}") - @preview_generate_error = "unable to execute mp3 convert command #{convert_output}" - else - ogg_digest = ::Digest::MD5.file(output) - mp3_digest = ::Digest::MD5.file(output_mp3) - self["preview_md5"] = ogg_md5 = ogg_digest.hexdigest - self["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest - - @@log.debug("uploading ogg preview to #{self.preview_filename('ogg')}") - s3_public_manager.upload(self.preview_filename(ogg_md5, 'ogg'), output, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest) - @@log.debug("uploading mp3 preview to #{self.preview_filename('mp3')}") - s3_public_manager.upload(self.preview_filename(mp3_md5, 'mp3'), output_mp3, content_type: 'audio/mpeg', content_md5: mp3_digest.base64) - - self.skip_uploader = true - - original_ogg_preview_url = self["preview_url"] - original_mp3_preview_url = self["preview_mp3_url"] - - # and finally update the JamTrackTrack with the new info - self["preview_url"] = self.preview_filename(ogg_md5, 'ogg') - self["preview_length"] = File.new(output).size - # and finally update the JamTrackTrack with the new info - self["preview_mp3_url"] = self.preview_filename(mp3_md5, 'mp3') - self["preview_mp3_length"] = File.new(output_mp3).size - self.save! - - # if all that worked, now delete old previews, if present - begin - s3_public_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != self["preview_url"] - s3_public_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"] - rescue - puts "UNABLE TO CLEANUP OLD PREVIEW URL" - end - - end - end - end - rescue Exception => e - @@log.error("error in sox command #{e.to_s}") - @preview_generate_error = e.to_s - end - - end - end diff --git a/db/manifest b/db/manifest index 756e8697b..9d147ec77 100755 --- a/db/manifest +++ b/db/manifest @@ -274,4 +274,16 @@ recording_client_metadata.sql preview_support_mp3.sql jam_track_duration.sql sales.sql -broadcast_notifications.sql +show_whats_next_count.sql +recurly_adjustments.sql +signup_hints.sql +packaging_notices.sql +first_played_jamtrack_at.sql +payment_history.sql +jam_track_right_private_key.sql +first_downloaded_jamtrack_at.sql +signing.sql +optimized_redeemption.sql +optimized_redemption_warn_mode.sql +affiliate_partners2.sql +broadcast_notifications.sql \ No newline at end of file diff --git a/db/up/affiliate_partners2.sql b/db/up/affiliate_partners2.sql new file mode 100644 index 000000000..e8488c8ce --- /dev/null +++ b/db/up/affiliate_partners2.sql @@ -0,0 +1,132 @@ +CREATE TABLE affiliate_legalese ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + legalese TEXT, + version INTEGER NOT NULL DEFAULT 1, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE users DROP CONSTRAINT users_affiliate_referral_id_fkey; +ALTER TABLE users DROP COLUMN affiliate_referral_id; +DROP TABLE affiliate_partners; + +CREATE TABLE affiliate_partners ( + id INTEGER PRIMARY KEY, + partner_name VARCHAR(1000), + partner_user_id VARCHAR(64) REFERENCES users(id) ON DELETE SET NULL, + entity_type VARCHAR(64), + legalese_id VARCHAR(64), + signed_at TIMESTAMP, + last_paid_at TIMESTAMP, + address JSON NOT NULL DEFAULT '{}', + tax_identifier VARCHAR(1000), + referral_user_count INTEGER NOT NULL DEFAULT 0, + cumulative_earnings_in_cents INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE SEQUENCE partner_key_sequence; +ALTER SEQUENCE partner_key_sequence RESTART WITH 10000; +ALTER TABLE affiliate_partners ALTER COLUMN id SET DEFAULT nextval('partner_key_sequence');; + +ALTER TABLE users ADD COLUMN affiliate_referral_id INTEGER REFERENCES affiliate_partners(id) ON DELETE SET NULL; +CREATE INDEX affiliate_partners_legalese_idx ON affiliate_partners(legalese_id); + +CREATE UNLOGGED TABLE affiliate_referral_visits ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + affiliate_partner_id INTEGER NOT NULL, + ip_address VARCHAR NOT NULL, + visited_url VARCHAR, + referral_url VARCHAR, + first_visit BOOLEAN NOT NULL DEFAULT TRUE, + user_id VARCHAR(64), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX on affiliate_referral_visits (affiliate_partner_id, created_at); + +CREATE TABLE affiliate_quarterly_payments ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + quarter INTEGER NOT NULL, + year INTEGER NOT NULL, + affiliate_partner_id INTEGER NOT NULL REFERENCES affiliate_partners(id), + due_amount_in_cents INTEGER NOT NULL DEFAULT 0, + paid BOOLEAN NOT NULL DEFAULT FALSE, + closed BOOLEAN NOT NULL DEFAULT FALSE, + jamtracks_sold INTEGER NOT NULL DEFAULT 0, + closed_at TIMESTAMP, + paid_at TIMESTAMP, + last_updated TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +CREATE TABLE affiliate_monthly_payments ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + month INTEGER NOT NULL, + year INTEGER NOT NULL, + affiliate_partner_id INTEGER NOT NULL REFERENCES affiliate_partners(id), + due_amount_in_cents INTEGER NOT NULL DEFAULT 0, + closed BOOLEAN NOT NULL DEFAULT FALSE, + jamtracks_sold INTEGER NOT NULL DEFAULT 0, + closed_at TIMESTAMP, + last_updated TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +CREATE INDEX ON affiliate_quarterly_payments (affiliate_partner_id, year, quarter); +CREATE UNIQUE INDEX ON affiliate_quarterly_payments (year, quarter, affiliate_partner_id); +CREATE UNIQUE INDEX ON affiliate_monthly_payments (year, month, affiliate_partner_id); +CREATE INDEX ON affiliate_monthly_payments (affiliate_partner_id, year, month); + + +ALTER TABLE sale_line_items ADD COLUMN affiliate_referral_id INTEGER REFERENCES affiliate_partners(id); +ALTER TABLE sale_line_items ADD COLUMN affiliate_referral_fee_in_cents INTEGER; +ALTER TABLE sale_line_items ADD COLUMN affiliate_refunded BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE sale_line_items ADD COLUMN affiliate_refunded_at TIMESTAMP; +ALTER TABLE generic_state ADD COLUMN affiliate_tallied_at TIMESTAMP; + +CREATE TABLE affiliate_traffic_totals ( + day DATE NOT NULL, + signups INTEGER NOT NULL DEFAULT 0, + visits INTEGER NOT NULL DEFAULT 0, + affiliate_partner_id INTEGER NOT NULL REFERENCES affiliate_partners(id), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX ON affiliate_traffic_totals (day, affiliate_partner_id); +CREATE INDEX ON affiliate_traffic_totals (affiliate_partner_id, day); + +CREATE VIEW affiliate_payments AS + SELECT id AS monthly_id, + CAST(NULL as VARCHAR) AS quarterly_id, + affiliate_partner_id, + due_amount_in_cents, + jamtracks_sold, + created_at, + closed, + CAST(NULL AS BOOLEAN) AS paid, + year, + month as month, + CAST(NULL AS INTEGER) as quarter, + month as time_sort, + 'monthly' AS payment_type + FROM affiliate_monthly_payments + UNION ALL + SELECT CAST(NULL as VARCHAR) AS monthly_id, + id AS quarterly_id, + affiliate_partner_id, + due_amount_in_cents, + jamtracks_sold, + created_at, + closed, + paid, + year, + CAST(NULL AS INTEGER) as month, + quarter, + (quarter * 3) + 3 as time_sort, + 'quarterly' AS payment_type + FROM affiliate_quarterly_payments; \ No newline at end of file diff --git a/db/up/first_downloaded_jamtrack_at.sql b/db/up/first_downloaded_jamtrack_at.sql new file mode 100644 index 000000000..3fdfb93cc --- /dev/null +++ b/db/up/first_downloaded_jamtrack_at.sql @@ -0,0 +1 @@ +ALTER TABLE jam_track_rights ADD COLUMN first_downloaded_at TIMESTAMP; \ No newline at end of file diff --git a/db/up/first_played_jamtrack_at.sql b/db/up/first_played_jamtrack_at.sql new file mode 100644 index 000000000..aeddf7757 --- /dev/null +++ b/db/up/first_played_jamtrack_at.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN first_played_jamtrack_at TIMESTAMP; \ No newline at end of file diff --git a/db/up/jam_track_duration.sql b/db/up/jam_track_duration.sql index a4dbee409..7c39e03ca 100644 --- a/db/up/jam_track_duration.sql +++ b/db/up/jam_track_duration.sql @@ -1 +1,9 @@ -ALTER TABLE jam_tracks ADD COLUMN duration INTEGER; \ No newline at end of file +DO $$ + BEGIN + BEGIN + ALTER TABLE jam_tracks ADD COLUMN duration INTEGER; + EXCEPTION + WHEN duplicate_column THEN RAISE NOTICE 'column duration already exists in jam_tracks.'; + END; + END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/db/up/jam_track_right_private_key.sql b/db/up/jam_track_right_private_key.sql new file mode 100644 index 000000000..a0e9b9d01 --- /dev/null +++ b/db/up/jam_track_right_private_key.sql @@ -0,0 +1,11 @@ +ALTER TABLE jam_track_rights ADD COLUMN private_key_44 VARCHAR; +ALTER TABLE jam_track_rights ADD COLUMN private_key_48 VARCHAR; +ALTER TABLE jam_track_rights ADD COLUMN signed_48 BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE jam_track_rights ADD COLUMN signed_44 BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE jam_track_rights DROP COLUMN private_key; +ALTER TABLE jam_track_rights DROP COLUMN signed; +ALTER TABLE jam_track_rights ADD COLUMN signing_started_at_44 TIMESTAMP; +ALTER TABLE jam_track_rights ADD COLUMN signing_started_at_48 TIMESTAMP; +ALTER TABLE jam_track_rights DROP COLUMN signing_started_at; + + diff --git a/db/up/optimized_redeemption.sql b/db/up/optimized_redeemption.sql new file mode 100644 index 000000000..476fef48d --- /dev/null +++ b/db/up/optimized_redeemption.sql @@ -0,0 +1,13 @@ +CREATE TABLE machine_fingerprints ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + fingerprint VARCHAR(20000) NOT NULL UNIQUE, + when_taken VARCHAR NOT NULL, + print_type VARCHAR NOT NULL, + remote_ip VARCHAR(1000) NOT NULL, + jam_track_right_id BIGINT REFERENCES jam_track_rights(id) ON DELETE SET NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE jam_track_rights ADD COLUMN redeemed_and_fingerprinted BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/db/up/optimized_redemption_warn_mode.sql b/db/up/optimized_redemption_warn_mode.sql new file mode 100644 index 000000000..faf2ad984 --- /dev/null +++ b/db/up/optimized_redemption_warn_mode.sql @@ -0,0 +1,42 @@ +DROP TABLE machine_fingerprints; + +CREATE TABLE machine_fingerprints ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + fingerprint VARCHAR(20000) NOT NULL, + when_taken VARCHAR NOT NULL, + print_type VARCHAR NOT NULL, + remote_ip VARCHAR(1000) NOT NULL, + jam_track_right_id BIGINT REFERENCES jam_track_rights(id) ON DELETE SET NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE machine_extras ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + machine_fingerprint_id VARCHAR(64) NOT NULL REFERENCES machine_fingerprints(id) ON DELETE CASCADE, + mac_address VARCHAR(100), + mac_name VARCHAR(255), + upstate BOOLEAN, + ipaddr_0 VARCHAR(200), + ipaddr_1 VARCHAR(200), + ipaddr_2 VARCHAR(200), + ipaddr_3 VARCHAR(200), + ipaddr_4 VARCHAR(200), + ipaddr_5 VARCHAR(200), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE fraud_alerts ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + machine_fingerprint_id VARCHAR(64) NOT NULL REFERENCES machine_fingerprints(id) ON DELETE CASCADE, + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + resolved BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE fingerprint_whitelists ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + fingerprint VARCHAR(20000) UNIQUE NOT NULL +); + +CREATE INDEX machine_fingerprints_index1 ON machine_fingerprints USING btree (fingerprint, user_id, remote_ip, created_at); diff --git a/db/up/packaging_notices.sql b/db/up/packaging_notices.sql new file mode 100644 index 000000000..d3a5de551 --- /dev/null +++ b/db/up/packaging_notices.sql @@ -0,0 +1,3 @@ +ALTER TABLE jam_track_rights ADD COLUMN packaging_steps INTEGER; +ALTER TABLE jam_track_rights ADD COLUMN current_packaging_step INTEGER; +ALTER TABLE jam_track_rights ADD COLUMN last_step_at TIMESTAMP; diff --git a/db/up/payment_history.sql b/db/up/payment_history.sql new file mode 100644 index 000000000..cb350c61d --- /dev/null +++ b/db/up/payment_history.sql @@ -0,0 +1,17 @@ +ALTER TABLE recurly_transaction_web_hooks ADD COLUMN admin_description VARCHAR; +ALTER TABLE recurly_transaction_web_hooks ADD COLUMN jam_track_id VARCHAR(64) REFERENCES jam_tracks(id); + +CREATE VIEW payment_histories AS + SELECT id AS sale_id, + CAST(NULL as VARCHAR) AS recurly_transaction_web_hook_id, + user_id, + created_at, + 'sale' AS transaction_type + FROM sales s + UNION ALL + SELECT CAST(NULL as VARCHAR) AS sale_id, + id AS recurly_transaction_web_hook_id, + user_id, + transaction_at AS created_at, + transaction_type + FROM recurly_transaction_web_hooks; \ No newline at end of file diff --git a/db/up/preview_support_mp3.sql b/db/up/preview_support_mp3.sql index 90dd1c642..8417e221b 100644 --- a/db/up/preview_support_mp3.sql +++ b/db/up/preview_support_mp3.sql @@ -1,4 +1,11 @@ -ALTER TABLE jam_track_tracks ADD COLUMN preview_mp3_url VARCHAR; -ALTER TABLE jam_track_tracks ADD COLUMN preview_mp3_md5 VARCHAR; -ALTER TABLE jam_track_tracks ADD COLUMN preview_mp3_length BIGINT; -UPDATE jam_track_tracks SET preview_url = NULL where track_type = 'Master'; \ No newline at end of file +DO $$ + BEGIN + BEGIN + ALTER TABLE jam_track_tracks ADD COLUMN preview_mp3_url VARCHAR; + ALTER TABLE jam_track_tracks ADD COLUMN preview_mp3_md5 VARCHAR; + ALTER TABLE jam_track_tracks ADD COLUMN preview_mp3_length BIGINT; + EXCEPTION + WHEN duplicate_column THEN RAISE NOTICE 'preview mp3 columns already exist in jam_tracks'; + END; + END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/db/up/recurly_adjustments.sql b/db/up/recurly_adjustments.sql new file mode 100644 index 000000000..dc31d21e9 --- /dev/null +++ b/db/up/recurly_adjustments.sql @@ -0,0 +1,24 @@ +ALTER TABLE sale_line_items ADD COLUMN recurly_adjustment_uuid VARCHAR(500); +ALTER TABLE sale_line_items ADD COLUMN recurly_adjustment_credit_uuid VARCHAR(500); +ALTER TABLE jam_track_rights ADD COLUMN recurly_adjustment_uuid VARCHAR(500); +ALTER TABLE jam_track_rights ADD COLUMN recurly_adjustment_credit_uuid VARCHAR(500); +ALTER TABLE sales ADD COLUMN recurly_invoice_id VARCHAR(500) UNIQUE; +ALTER TABLE sales ADD COLUMN recurly_invoice_number INTEGER; + +ALTER TABLE sales ADD COLUMN recurly_subtotal_in_cents INTEGER; +ALTER TABLE sales ADD COLUMN recurly_tax_in_cents INTEGER; +ALTER TABLE sales ADD COLUMN recurly_total_in_cents INTEGER; +ALTER TABLE sales ADD COLUMN recurly_currency VARCHAR; + +ALTER TABLE sale_line_items ADD COLUMN recurly_tax_in_cents INTEGER; +ALTER TABLE sale_line_items ADD COLUMN recurly_total_in_cents INTEGER; +ALTER TABLE sale_line_items ADD COLUMN recurly_currency VARCHAR; +ALTER TABLE sale_line_items ADD COLUMN recurly_discount_in_cents INTEGER; + +ALTER TABLE sales ADD COLUMN sale_type VARCHAR NOT NULL DEFAULT 'jamtrack'; + +ALTER TABLE recurly_transaction_web_hooks ALTER COLUMN subscription_id DROP NOT NULL; + +CREATE INDEX recurly_transaction_web_hooks_invoice_id_ndx ON recurly_transaction_web_hooks(invoice_id); + +ALTER TABLE jam_track_rights DROP COLUMN recurly_subscription_uuid; \ No newline at end of file diff --git a/db/up/show_whats_next_count.sql b/db/up/show_whats_next_count.sql new file mode 100644 index 000000000..82e50d469 --- /dev/null +++ b/db/up/show_whats_next_count.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN show_whats_next_count INTEGER NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/db/up/signing.sql b/db/up/signing.sql new file mode 100644 index 000000000..3de748aae --- /dev/null +++ b/db/up/signing.sql @@ -0,0 +1,2 @@ +ALTER TABLE jam_track_rights ADD COLUMN signing_44 BOOLEAN DEFAULT FALSE; +ALTER TABLE jam_track_rights ADD COLUMN signing_48 BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/db/up/signup_hints.sql b/db/up/signup_hints.sql new file mode 100644 index 000000000..9a079e216 --- /dev/null +++ b/db/up/signup_hints.sql @@ -0,0 +1,12 @@ +CREATE TABLE signup_hints ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + anonymous_user_id VARCHAR(64) UNIQUE, + redirect_location VARCHAR, + want_jamblaster BOOLEAN NOT NULL DEFAULT FALSE, + user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE, + expires_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE users ADD COLUMN want_jamblaster BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index b8b675f4f..8c1a51a58 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -20,11 +20,12 @@ require 'resque_mailer' require 'rest-client' require 'zip' require 'csv' +require 'tzinfo' require "jam_ruby/constants/limits" require "jam_ruby/constants/notification_types" require "jam_ruby/constants/validation_messages" -require "jam_ruby/errors/permission_error" +require "jam_ruby/errors/jam_permission_error" require "jam_ruby/errors/state_error" require "jam_ruby/errors/jam_argument_error" require "jam_ruby/errors/conflict_error" @@ -58,10 +59,12 @@ require "jam_ruby/resque/scheduled/score_history_sweeper" require "jam_ruby/resque/scheduled/scheduled_music_session_cleaner" require "jam_ruby/resque/scheduled/recordings_cleaner" require "jam_ruby/resque/scheduled/jam_tracks_cleaner" -require "jam_ruby/resque/jam_tracks_builder" require "jam_ruby/resque/scheduled/stats_maker" +require "jam_ruby/resque/scheduled/tally_affiliates" +require "jam_ruby/resque/jam_tracks_builder" require "jam_ruby/resque/google_analytics_event" require "jam_ruby/resque/batch_email_job" +require "jam_ruby/resque/long_running" require "jam_ruby/mq_router" require "jam_ruby/recurly_client" require "jam_ruby/base_manager" @@ -69,6 +72,7 @@ require "jam_ruby/connection_manager" require "jam_ruby/version" require "jam_ruby/environment" require "jam_ruby/init" +require "jam_ruby/app/mailers/admin_mailer" require "jam_ruby/app/mailers/user_mailer" require "jam_ruby/app/mailers/invited_user_mailer" require "jam_ruby/app/mailers/corp_mailer" @@ -99,6 +103,11 @@ require "jam_ruby/models/genre_player" require "jam_ruby/models/genre" require "jam_ruby/models/user" require "jam_ruby/models/anonymous_user" +require "jam_ruby/models/signup_hint" +require "jam_ruby/models/machine_fingerprint" +require "jam_ruby/models/machine_extra" +require "jam_ruby/models/fraud_alert" +require "jam_ruby/models/fingerprint_whitelist" require "jam_ruby/models/rsvp_request" require "jam_ruby/models/rsvp_slot" require "jam_ruby/models/rsvp_request_rsvp_slot" @@ -198,12 +207,19 @@ 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/affiliate_legalese" +require "jam_ruby/models/affiliate_quarterly_payment" +require "jam_ruby/models/affiliate_monthly_payment" +require "jam_ruby/models/affiliate_traffic_total" +require "jam_ruby/models/affiliate_referral_visit" +require "jam_ruby/models/affiliate_payment" 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" require "jam_ruby/models/user_sync" +require "jam_ruby/models/payment_history" require "jam_ruby/models/video_source" require "jam_ruby/models/text_message" require "jam_ruby/models/sale" diff --git a/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb new file mode 100644 index 000000000..f14d8e424 --- /dev/null +++ b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb @@ -0,0 +1,37 @@ +module JamRuby + # sends out a boring ale + class AdminMailer < ActionMailer::Base + include SendGrid + + + DEFAULT_SENDER = "JamKazam " + + default :from => DEFAULT_SENDER + + sendgrid_category :use_subject_lines + #sendgrid_enable :opentrack, :clicktrack # this makes our emails creepy, imo (seth) + sendgrid_unique_args :env => Environment.mode + + def alerts(options) + mail(to: APP_CONFIG.email_alerts_alias, + from: APP_CONFIG.email_generic_from, + body: options[:body], + content_type: "text/plain", + subject: options[:subject]) + end + + def recurly_alerts(user, options) + + body = options[:body] + body << "\n\n" + body << "User " << user.admin_url + "\n" + body << "User's JamTracks " << user.jam_track_rights_admin_url + "\n" + + mail(to: APP_CONFIG.email_recurly_notice, + from: APP_CONFIG.email_generic_from, + body: body, + content_type: "text/plain", + subject: options[:subject]) + end + end +end diff --git a/ruby/lib/jam_ruby/app/mailers/invited_user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/invited_user_mailer.rb index ed772126b..dab372c1f 100644 --- a/ruby/lib/jam_ruby/app/mailers/invited_user_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/invited_user_mailer.rb @@ -52,7 +52,7 @@ module JamRuby def generate_signup_url(invited_user) invited_user.generate_signup_url - # "http://www.jamkazam.com/signup?invitation_code=#{invited_user.invitation_code}" + # "https://www.jamkazam.com/signup?invitation_code=#{invited_user.invitation_code}" end end end diff --git a/ruby/lib/jam_ruby/app/uploaders/jam_track_right_uploader.rb b/ruby/lib/jam_ruby/app/uploaders/jam_track_right_uploader.rb index e33494063..520908b7b 100644 --- a/ruby/lib/jam_ruby/app/uploaders/jam_track_right_uploader.rb +++ b/ruby/lib/jam_ruby/app/uploaders/jam_track_right_uploader.rb @@ -23,6 +23,6 @@ class JamTrackRightUploader < CarrierWave::Uploader::Base end def filename - "#{model.store_dir}/#{model.filename}" if model.id + "#{model.store_dir}/#{model.filename(mounted_as)}" if model.id end end diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/corp_mailer/feedback.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/corp_mailer/feedback.html.erb index 2f21454bf..febb229c0 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/corp_mailer/feedback.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/corp_mailer/feedback.html.erb @@ -8,7 +8,7 @@

-This email was received because someone left feedback at http://www.jamkazam.com/corp/contact +This email was received because someone left feedback at http://www.jamkazam.com/corp/contact

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/corp_mailer/feedback.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/corp_mailer/feedback.text.erb index 4612cf5ee..ee99b3544 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/corp_mailer/feedback.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/corp_mailer/feedback.text.erb @@ -5,4 +5,4 @@ From <%= @email %>: <%= @body %> -This email was received because someone left feedback at http://www.jamkazam.com/corp/contact \ No newline at end of file +This email was received because someone left feedback at https://www.jamkazam.com/corp/contact \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_notdl.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_notdl.html.erb index 5ce39a3c6..cb0654e8d 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_notdl.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_notdl.html.erb @@ -7,7 +7,7 @@

-Go to Download Page +Go to Download Page

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_notdl.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_notdl.text.erb index 4ec636c7b..cfda033fb 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_notdl.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_notdl.text.erb @@ -4,7 +4,7 @@ Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- We noticed that you have registered as a JamKazam musician, but you have not yet downloaded and started using the free JamKazam application. You can find other musicians and listen to sessions and recordings on our website, but you need the free JamKazam application to play with other musicians online. Please click the link below to go to the download page for the free JamKazam application, or visit our JamKazam support center so that we can help you get up and running. -Go to Download Page: http://www.jamkazam.com/downloads +Go to Download Page: https://www.jamkazam.com/downloads Go to Support Center: https://jamkazam.desk.com diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/gear_notsess.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/gear_notsess.html.erb index 0a72b2017..5ecea0b58 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/gear_notsess.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/gear_notsess.html.erb @@ -10,7 +10,7 @@ It’s still very early in our company’s development, so we don’t have zillions of users online on our service yet. If you click Find Session, you will often not find a good session to join, both due to the number of musicians online at any given time, and also because you won’t see private sessions where groups of musicians don’t want to be interrupted in their sessions.

-

If you are having trouble getting into sessions, we’d suggest you click the Musicians tile on the home screen of the app or the website: Go To Musicians Page +

If you are having trouble getting into sessions, we’d suggest you click the Musicians tile on the home screen of the app or the website: Go To Musicians Page

This will display the JamKazam musicians sorted by latency to you - in other words, you can see which musicians have good network connections to you. Any musicians with green and yellow latency scores have good enough connections to support a play session with you. We recommend that read the profiles of these musicians to find others with shared musical interests and good network connections to you, and then use the Message button to say hi and see if they are interested in playing with you. If they are, use the Connect button to “friend” them on JamKazam, and use the Message button to set up a time to meet online for a session. diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/gear_notsess.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/gear_notsess.text.erb index d027cf178..e5f74495a 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/gear_notsess.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/gear_notsess.text.erb @@ -7,7 +7,7 @@ We noticed that you haven’t yet played in a JamKazam session with multiple mus Find Other Musicians on JamKazam It’s still very early in our company’s development, so we don’t have zillions of users online on our service yet. If you click Find Session, you will often not find a good session to join, both due to the number of musicians online at any given time, and also because you won’t see private sessions where groups of musicians don’t want to be interrupted in their sessions. -If you are having trouble getting into sessions, we’d suggest you click the Musicians tile on the home screen of the app or the website: http://www.jamkazam.com/client#/musicians +If you are having trouble getting into sessions, we’d suggest you click the Musicians tile on the home screen of the app or the website: https://www.jamkazam.com/client#/musicians This will display the JamKazam musicians sorted by latency to you - in other words, you can see which musicians have good network connections to you. Any musicians with green and yellow latency scores have good enough connections to support a play session with you. We recommend that read the profiles of these musicians to find others with shared musical interests and good network connections to you, and then use the Message button to say hi and see if they are interested in playing with you. If they are, use the Connect button to “friend” them on JamKazam, and use the Message button to set up a time to meet online for a session. diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notconnect.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notconnect.html.erb index 0a0eada21..cf444d605 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notconnect.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notconnect.html.erb @@ -7,7 +7,7 @@

Find Other Musicians on JamKazam
-To find and connect with other musicians who are already on JamKazam, we’d suggest you click the Musicians tile on the home screen of the app or the website: Go To Musicians Page +To find and connect with other musicians who are already on JamKazam, we’d suggest you click the Musicians tile on the home screen of the app or the website: Go To Musicians Page

This will display the JamKazam musicians sorted by latency to you - in other words, you can see which musicians have good network connections to you. Any musicians with green and yellow latency scores have good enough connections to support a play session with you. We recommend that you read the profiles of these musicians to find others with shared musical interests and good network connections to you, and then use the Message button to say hi and see if they are interested in playing with you. If they are, use the Connect button to “friend” them on JamKazam, and use the Message button to set up a time to meet online for a session. diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notconnect.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notconnect.text.erb index 370823a10..cd90de475 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notconnect.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notconnect.text.erb @@ -5,7 +5,7 @@ Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- We noticed that you haven’t yet connected with any friends on JamKazam. Connecting with friends is the best way to help you get into sessions with other musicians on JamKazam. Here are a couple of good ways to connect with others. Find Other Musicians on JamKazam -To find and connect with other musicians who are already on JamKazam, we’d suggest you click the Musicians tile on the home screen of the app or the website: http://www.jamkazam.com/client#/musicians +To find and connect with other musicians who are already on JamKazam, we’d suggest you click the Musicians tile on the home screen of the app or the website: https://www.jamkazam.com/client#/musicians This will display the JamKazam musicians sorted by latency to you - in other words, you can see which musicians have good network connections to you. Any musicians with green and yellow latency scores have good enough connections to support a play session with you. We recommend that you read the profiles of these musicians to find others with shared musical interests and good network connections to you, and then use the Message button to say hi and see if they are interested in playing with you. If they are, use the Connect button to “friend” them on JamKazam, and use the Message button to set up a time to meet online for a session. diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notlike.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notlike.html.erb index 3b654a83a..e9c3c4d03 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notlike.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notlike.html.erb @@ -7,7 +7,7 @@

<% [:twitter, :facebook, :google].each do |site| %> - <%= link_to(image_tag("http://www.jamkazam.com/assets/content/icon_#{site}.png", :style => "vertical-align:top"), "http://www.jamkazam.com/endorse/@USERID/#{site}?src=email") %>  + <%= link_to(image_tag("https://www.jamkazam.com/assets/content/icon_#{site}.png", :style => "vertical-align:top"), "https://www.jamkazam.com/endorse/@USERID/#{site}?src=email") %>  <% end %>

-- Team JamKazam diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notlike.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notlike.text.erb index 9dddb6ca6..37c4a7623 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notlike.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notlike.text.erb @@ -5,7 +5,7 @@ Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- JamKazam is a young company/service built through the sweat and commitment of a small group of music-loving techies. Please help us continue to grow the service and attract more musicians to play online by liking and/or following us on Facebook, Twitter, and Google+. Just click the icons below to give us little push, thanks! <% [:twitter, :facebook, :google].each do |site| %> - http://www.jamkazam.com/endorse/@USERID/#{site}?src=email + https://www.jamkazam.com/endorse/@USERID/#{site}?src=email <% end %> diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb index 7a25bfe43..be9aa27fb 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb @@ -24,7 +24,7 @@ Hi <%= @user.first_name %>, <% end %>

-

There are currently <%= @new_musicians.size%> musicians on JamKazam with low enough latency Internet connections to you to support a good online session. To see ALL the JamKazam musicians with whom you may want to connect and play, view our Musicians page at: http://www.jamkazam.com/client#/musicians. +

There are currently <%= @new_musicians.size%> musicians on JamKazam with low enough latency Internet connections to you to support a good online session. To see ALL the JamKazam musicians with whom you may want to connect and play, view our Musicians page at: http://www.jamkazam.com/client#/musicians.

Best Regards,

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb index df39c005a..05fbbd268 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb @@ -11,7 +11,7 @@ The following new musicians have joined JamKazam within the last week, and have <%= user.biography %> <% end %> -There are currently <%= @new_musicians.size%> musicians on JamKazam with low enough latency Internet connections to you to support a good online session. To see ALL the JamKazam musicians with whom you may want to connect and play, view our Musicians page at: http://www.jamkazam.com/client#/musicians. +There are currently <%= @new_musicians.size%> musicians on JamKazam with low enough latency Internet connections to you to support a good online session. To see ALL the JamKazam musicians with whom you may want to connect and play, view our Musicians page at: https://www.jamkazam.com/client#/musicians. Best Regards, Team JamKazam diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.html.erb index c4b476d45..2fdc11256 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.html.erb @@ -68,16 +68,16 @@ <%= sess.genre.description %> <%= sess.name %>
- ">Details + ">Details <%= sess.description %> <%= (sess.latency / 2).round %> ms <% if sess.latency <= (APP_CONFIG.max_good_full_score / 2) %> - <%= image_tag("http://www.jamkazam.com/assets/content/icon_green_score.png", alt: 'good score icon') %> + <%= image_tag("https://www.jamkazam.com/assets/content/icon_green_score.png", alt: 'good score icon') %> <% else %> - <%= image_tag("http://www.jamkazam.com/assets/content/icon_yellow_score.png", alt: 'fair score icon') %> + <%= image_tag("https://www.jamkazam.com/assets/content/icon_yellow_score.png", alt: 'fair score icon') %> <% end %> @@ -86,7 +86,7 @@ -

To see ALL the scheduled sessions that you might be interested in joining, view our Find Session page.

+

To see ALL the scheduled sessions that you might be interested in joining, view our Find Session page.

Best Regards,

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.text.erb index 9720550d5..282c6dd90 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.text.erb @@ -9,7 +9,7 @@ GENRE | NAME | DESCRIPTION | LATENCY <%= sess.genre.description %> | <%= sess.name %> | <%= sess.description %> | <%= sess.latency %> ms <% end %> -To see ALL the scheduled sessions that you might be interested in joining, view our Find Session page at: http://www.jamkazam.com/client#/findSession. +To see ALL the scheduled sessions that you might be interested in joining, view our Find Session page at: https://www.jamkazam.com/client#/findSession. Best Regards, diff --git a/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.html.erb b/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.html.erb index 6b10f7f66..5b7ce4aec 100644 --- a/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.html.erb +++ b/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.html.erb @@ -19,7 +19,7 @@ - +
JamKazamJamKazam
@@ -51,7 +51,7 @@

-

This email was sent to you because you have an account at JamKazam. +

This email was sent to you because you have an account at JamKazam.

diff --git a/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.text.erb b/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.text.erb index 8bd3c7483..88d4f9234 100644 --- a/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.text.erb +++ b/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.text.erb @@ -2,7 +2,7 @@ <% unless @suppress_user_has_account_footer == true %> -This email was sent to you because you have an account at JamKazam / http://www.jamkazam.com. +This email was sent to you because you have an account at JamKazam / https://www.jamkazam.com. <% end %> Copyright <%= Time.now.year %> JamKazam, Inc. All rights reserved. diff --git a/ruby/lib/jam_ruby/app/views/layouts/user_mailer.html.erb b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.html.erb index 3b11eebd1..4f184062f 100644 --- a/ruby/lib/jam_ruby/app/views/layouts/user_mailer.html.erb +++ b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.html.erb @@ -39,7 +39,7 @@ -

This email was sent to you because you have an account at JamKazam.  Click here to unsubscribe and update your profile settings. +

This email was sent to you because you have an account at JamKazam.  Click here to unsubscribe and update your profile settings.

diff --git a/ruby/lib/jam_ruby/app/views/layouts/user_mailer.text.erb b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.text.erb index 5c8262f63..03accee00 100644 --- a/ruby/lib/jam_ruby/app/views/layouts/user_mailer.text.erb +++ b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.text.erb @@ -4,8 +4,8 @@ <%= yield %> <% end %> -<% unless @suppress_user_has_account_footer == true %> -This email was sent to you because you have an account at JamKazam / http://www.jamkazam.com. Visit your profile page to unsubscribe: http://www.jamkazam.com/client#/account/profile. +<% unless @user.nil? || @suppress_user_has_account_footer == true %> +This email was sent to you because you have an account at JamKazam / https://www.jamkazam.com. To unsubscribe: https://www.jamkazam.com/unsubscribe/<%=@user.unsubscribe_token%>. <% end %> Copyright <%= Time.now.year %> JamKazam, Inc. All rights reserved. diff --git a/ruby/lib/jam_ruby/errors/jam_permission_error.rb b/ruby/lib/jam_ruby/errors/jam_permission_error.rb new file mode 100644 index 000000000..531791df1 --- /dev/null +++ b/ruby/lib/jam_ruby/errors/jam_permission_error.rb @@ -0,0 +1,5 @@ +module JamRuby + class JamPermissionError < Exception + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/errors/permission_error.rb b/ruby/lib/jam_ruby/errors/permission_error.rb deleted file mode 100644 index f0b4e3a2f..000000000 --- a/ruby/lib/jam_ruby/errors/permission_error.rb +++ /dev/null @@ -1,5 +0,0 @@ -module JamRuby - class PermissionError < Exception - - end -end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/jam_track_importer.rb b/ruby/lib/jam_ruby/jam_track_importer.rb index 58138dbf9..70ef72e6a 100644 --- a/ruby/lib/jam_ruby/jam_track_importer.rb +++ b/ruby/lib/jam_ruby/jam_track_importer.rb @@ -91,13 +91,15 @@ module JamRuby true end - def synchronize_metadata(jam_track, metadata, metalocation, original_artist, name) + def synchronize_metadata(jam_track, metadata, metalocation, original_artist, name, options) metadata ||= {} self.name = metadata["name"] || name if jam_track.new_record? - jam_track.id = "#{JamTrack.count + 1}" # default is UUID, but the initial import was based on auto-increment ID, so we'll maintain that + latest_jamtrack = JamTrack.order('created_at desc').first + id = latest_jamtrack.nil? ? 1 : latest_jamtrack.id.to_i + 1 + jam_track.id = "#{id}" # default is UUID, but the initial import was based on auto-increment ID, so we'll maintain that jam_track.status = 'Staging' jam_track.metalocation = metalocation jam_track.original_artist = metadata["original_artist"] || original_artist @@ -107,13 +109,20 @@ module JamRuby jam_track.price = 1.99 jam_track.reproduction_royalty_amount = 0 jam_track.licensor_royalty_amount = 0 - jam_track.sales_region = 'United States' + jam_track.sales_region = 'Worldwide' jam_track.recording_type = 'Cover' jam_track.description = "This is a JamTrack audio file for use exclusively with the JamKazam service. This JamTrack is a high quality cover of the #{jam_track.original_artist} song \"#{jam_track.name}\"." else - #@@log.debug("#{self.name} skipped because it already exists in database") - finish("jam_track_exists", "") - return false + if !options[:resync_audio] + #@@log.debug("#{self.name} skipped because it already exists in database") + finish("jam_track_exists", "") + return false + else + # jamtrack exists, leave it be + return true + end + + end saved = jam_track.save @@ -318,24 +327,31 @@ module JamRuby def set_custom_weight(track) weight = 5 - case track.instrument_id - when 'electric guitar' - weight = 1 - when 'acoustic guitar' - weight = 2 - when 'drums' - weight = 3 - when 'keys' - weight = 4 - when 'computer' - weight = 10 - else - weight = 5 - end - if track.track_type == 'Master' - weight = 1000 + # if there are any persisted tracks, do not sort from scratch; just stick new stuff at the end + + if track.persisted? + weight = track.position + else + case track.instrument_id + when 'electric guitar' + weight = 100 + when 'acoustic guitar' + weight = 200 + when 'drums' + weight = 300 + when 'keys' + weight = 400 + when 'computer' + weight = 600 + else + weight = 500 + end + if track.track_type == 'Master' + weight = 1000 + end end + weight end @@ -346,10 +362,19 @@ module JamRuby a_weight <=> b_weight end + # default to 1, but if there are any persisted tracks, this will get manipulated to be +1 the highest persisted track position = 1 sorted_tracks.each do |track| - track.position = position - position = position + 1 + if track.persisted? + # persisted tracks should be sorted at the beginning of the sorted_tracks, + # so this just keeps moving the 'position builder' up to +1 of the last persisted track + position = track.position + 1 + else + track.position = position + position = position + 1 + end + + end sorted_tracks[sorted_tracks.length - 1].position = 1000 @@ -359,11 +384,40 @@ module JamRuby def synchronize_audio(jam_track, metadata, s3_path, skip_audio_upload) + attempt_to_match_existing_tracks = true + + # find all wav files in the JamTracks s3 bucket wav_files = fetch_wav_files(s3_path) tracks = [] wav_files.each do |wav_file| + + if attempt_to_match_existing_tracks + # try to find a matching track from the JamTrack based on the name of the 44.1 path + basename = File.basename(wav_file) + ogg_44100_filename = File.basename(basename, ".wav") + "-44100.ogg" + + found_track = nil + jam_track.jam_track_tracks.each do |jam_track_track| + + if jam_track_track["url_44"] && jam_track_track["url_44"].end_with?(ogg_44100_filename) + # found a match! + found_track = jam_track_track + break + end + end + + if found_track + @@log.debug("found a existing track to reuse") + found_track.original_audio_s3_path = wav_file + tracks << found_track + next + end + end + + @@log.debug("no existing track found; creating a new one") + track = JamTrackTrack.new track.original_audio_s3_path = wav_file @@ -388,6 +442,15 @@ module JamRuby tracks << track end + jam_track.jam_track_tracks.each do |jam_track_track| + # delete all jam_track_tracks not in the tracks array + unless tracks.include?(jam_track_track) + @@log.info("destroying removed JamTrackTrack #{jam_track_track.inspect}") + jam_track_track.destroy # should also delete s3 files associated with this jamtrack + end + end + + @@log.info("sorting tracks") tracks = sort_tracks(tracks) jam_track.jam_track_tracks = tracks @@ -481,15 +544,16 @@ module JamRuby track["length_48"] = File.new(ogg_48000).size synchronize_duration(jam_track, ogg_44100) + jam_track.save! # convert entire master ogg file to mp3, and push both to public destination - preview_succeeded = synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_44100_digest) if track.track_type == 'Master' + if track.track_type == 'Master' + preview_succeeded = synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_44100_digest) - if !preview_succeeded - return false + if !preview_succeeded + return false + end end - - end track.save! @@ -596,7 +660,7 @@ module JamRuby original_artist = parsed_metalocation[1] name = parsed_metalocation[2] - success = synchronize_metadata(jam_track, metadata, metalocation, original_artist, name) + success = synchronize_metadata(jam_track, metadata, metalocation, original_artist, name, options) return unless success @@ -614,7 +678,8 @@ module JamRuby def synchronize_recurly(jam_track) begin recurly = RecurlyClient.new - recurly.create_jam_track_plan(jam_track) unless recurly.find_jam_track_plan(jam_track) + # no longer create JamTrack plans: VRFS-3028 + # recurly.create_jam_track_plan(jam_track) unless recurly.find_jam_track_plan(jam_track) rescue RecurlyClientError => x finish('recurly_create_plan', x.errors.to_s) return false @@ -654,6 +719,33 @@ module JamRuby end + def synchronize_preview(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + + error_occurred = false + error_msg = nil + jam_track.jam_track_tracks.each do |track| + next if track.track_type == 'Master' + + if track.preview_start_time + track.generate_preview + if track.preview_generate_error + error_occurred = true + error_msg = track.preview_generate_error + else + end + end + end + + if error_occurred + importer.finish('preview_error', error_msg) + else + importer.finish('success', nil) + end + importer + end + def synchronize_jamtrack_master_preview(jam_track) importer = JamTrackImporter.new importer.name = jam_track.name @@ -676,6 +768,32 @@ module JamRuby importer end + + def synchronize_previews + importers = [] + + JamTrack.all.each do |jam_track| + importers << synchronize_preview(jam_track) + end + + @@log.info("SUMMARY") + @@log.info("-------") + importers.each do |importer| + if importer + if importer.reason == "success" || importer.reason == "no_preview_start_time" + @@log.info("#{importer.name} #{importer.reason}") + else + @@log.error("#{importer.name} failed to import.") + @@log.error("#{importer.name} reason=#{importer.reason}") + @@log.error("#{importer.name} detail=#{importer.detail}") + end + else + @@log.error("NULL IMPORTER") + end + + end + end + def synchronize_jamtrack_master_previews importers = [] diff --git a/ruby/lib/jam_ruby/jam_tracks_manager.rb b/ruby/lib/jam_ruby/jam_tracks_manager.rb index 9322c3dbd..d6ae37b1e 100644 --- a/ruby/lib/jam_ruby/jam_tracks_manager.rb +++ b/ruby/lib/jam_ruby/jam_tracks_manager.rb @@ -22,9 +22,21 @@ module JamRuby save_jam_track_right_jkz(jam_track_right, sample_rate) end + # increment the step, which causes a notification to be sent to the client so it can keep the UI fresh as the packaging step goes on + def bump_step(jam_track_right, step) + last_step_at = Time.now + jam_track_right.current_packaging_step = step + jam_track_right.last_step_at = Time.now + JamTrackRight.where(:id => jam_track_right.id).update_all(last_step_at: last_step_at, current_packaging_step: step) + SubscriptionMessage.jam_track_signing_job_change(jam_track_right) + step = step + 1 + step + end + def save_jam_track_right_jkz(jam_track_right, sample_rate=48) jam_track = jam_track_right.jam_track py_root = APP_CONFIG.jamtracks_dir + step = 0 Dir.mktmpdir do |tmp_dir| jam_file_opts="" jam_track.jam_track_tracks.each do |jam_track_track| @@ -36,6 +48,9 @@ module JamRuby track_filename = File.join(tmp_dir, nm) track_url = jam_track_track.sign_url(120, sample_rate) @@log.info("downloading #{track_url} to #{track_filename}") + + step = bump_step(jam_track_right, step) + copy_url_to_file(track_url, track_filename) jam_file_opts << " -i #{Shellwords.escape("#{track_filename}+#{jam_track_track.part}")}" end @@ -48,6 +63,8 @@ module JamRuby version = jam_track.version @@log.info "Executing python source in #{py_file}, outputting to #{tmp_dir} (#{output_jkz})" + step = bump_step(jam_track_right, step) + # From http://stackoverflow.com/questions/690151/getting-output-of-system-calls-in-ruby/5970819#5970819: cli = "python #{py_file} -D -k #{sku} -p #{Shellwords.escape(tmp_dir)}/pkey.pem -s #{Shellwords.escape(tmp_dir)}/skey.pem #{jam_file_opts} -o #{Shellwords.escape(output_jkz)} -t #{Shellwords.escape(title)} -V #{Shellwords.escape(version)}" Open3.popen3(cli) do |stdin, stdout, stderr, wait_thr| @@ -65,10 +82,18 @@ module JamRuby else jam_track_right.url_44.store!(File.open(output_jkz, "rb")) end - - jam_track_right.signed=true - jam_track_right.downloaded_since_sign=false - jam_track_right.private_key=File.read("#{tmp_dir}/skey.pem") + + if sample_rate == 48 + jam_track_right.signed_48 = true + jam_track_right.private_key_48 = File.read("#{tmp_dir}/skey.pem") + else + jam_track_right.signed_44 = true + jam_track_right.private_key_44 = File.read("#{tmp_dir}/skey.pem") + end + + jam_track_right.signing_queued_at = nil # if left set, throws off signing_state on subsequent signing attempts + jam_track_right.downloaded_since_sign = false + jam_track_right.save! end end # mktmpdir @@ -78,7 +103,7 @@ module JamRuby def copy_url_to_file(url, filename) uri = URI(url) open(filename, 'w+b') do |io| - Net::HTTP.start(uri.host, uri.port) do |http| + Net::HTTP.start(uri.host, uri.port, use_ssl: url.start_with?('https') ? true : false) do |http| request = Net::HTTP::Get.new uri http.request request do |response| response_code = response.code.to_i diff --git a/ruby/lib/jam_ruby/lib/subscription_message.rb b/ruby/lib/jam_ruby/lib/subscription_message.rb index ec161ec8f..6be9f7d16 100644 --- a/ruby/lib/jam_ruby/lib/subscription_message.rb +++ b/ruby/lib/jam_ruby/lib/subscription_message.rb @@ -22,7 +22,7 @@ module JamRuby end def self.jam_track_signing_job_change(jam_track_right) - Notification.send_subscription_message('jam_track_right', jam_track_right.id.to_s, {signing_state: jam_track_right.signing_state}.to_json ) + Notification.send_subscription_message('jam_track_right', jam_track_right.id.to_s, {signing_state: jam_track_right.signing_state, current_packaging_step: jam_track_right.current_packaging_step, packaging_steps: jam_track_right.packaging_steps}.to_json ) end end end diff --git a/ruby/lib/jam_ruby/models/active_music_session.rb b/ruby/lib/jam_ruby/models/active_music_session.rb index 04724695c..182157e23 100644 --- a/ruby/lib/jam_ruby/models/active_music_session.rb +++ b/ruby/lib/jam_ruby/models/active_music_session.rb @@ -7,7 +7,7 @@ module JamRuby self.table_name = 'active_music_sessions' - attr_accessor :legal_terms, :max_score, :opening_jam_track, :opening_recording, :opening_backing_track, :opening_metronome + attr_accessor :legal_terms, :max_score, :opening_jam_track, :opening_recording, :opening_backing_track, :opening_metronome, :jam_track_id belongs_to :claimed_recording, :class_name => "JamRuby::ClaimedRecording", :foreign_key => "claimed_recording_id", :inverse_of => :playing_sessions belongs_to :claimed_recording_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_claimed_recordings, :foreign_key => "claimed_recording_initiator_id" @@ -774,6 +774,7 @@ module JamRuby self.opening_jam_track = true self.save self.opening_jam_track = false + #self.tick_track_changes end def close_jam_track @@ -823,5 +824,19 @@ module JamRuby music_session.band_id = session_history.band.id unless session_history.band.nil? session_history.save! end + + def self.stats + stats = {} + + result = ActiveMusicSession.select('count(distinct(id)) AS total, count(distinct(jam_track_initiator_id)) as jam_track_count, count(distinct(backing_track_initiator_id)) as backing_track_count, count(distinct(metronome_initiator_id)) as metronome_count, count(distinct(claimed_recording_initiator_id)) as recording_count').first + + stats['count'] = result['total'].to_i + stats['jam_track_count'] = result['jam_track_count'].to_i + stats['backing_track_count'] = result['backing_track_count'].to_i + stats['metronome_count'] = result['metronome_count'].to_i + stats['recording_count'] = result['recording_count'].to_i + + stats + end end end diff --git a/ruby/lib/jam_ruby/models/affiliate_legalese.rb b/ruby/lib/jam_ruby/models/affiliate_legalese.rb new file mode 100644 index 000000000..732d45025 --- /dev/null +++ b/ruby/lib/jam_ruby/models/affiliate_legalese.rb @@ -0,0 +1,6 @@ +class JamRuby::AffiliateLegalese < ActiveRecord::Base + self.table_name = 'affiliate_legalese' + + has_many :affiliate_partners, :class_name => "JamRuby::AffiliatePartner", :foreign_key => :legalese_id + +end diff --git a/ruby/lib/jam_ruby/models/affiliate_monthly_payment.rb b/ruby/lib/jam_ruby/models/affiliate_monthly_payment.rb new file mode 100644 index 000000000..8aa45ca5a --- /dev/null +++ b/ruby/lib/jam_ruby/models/affiliate_monthly_payment.rb @@ -0,0 +1,39 @@ +class JamRuby::AffiliateMonthlyPayment < ActiveRecord::Base + + belongs_to :affiliate_partner, class_name: 'JamRuby::AffiliatePartner', inverse_of: :months + + + def self.index(user, options) + unless user.affiliate_partner + return [[], nil] + end + + page = options[:page].to_i + per_page = options[:per_page].to_i + + if page == 0 + page = 1 + end + + if per_page == 0 + per_page = 50 + end + + start = (page -1 ) * per_page + limit = per_page + + query = AffiliateMonthlyPayment + .paginate(page: page, per_page: per_page) + .where(affiliate_partner_id: user.affiliate_partner.id) + .order('year ASC, month ASC') + + if query.length == 0 + [query, nil] + elsif query.length < limit + [query, nil] + else + [query, start + limit] + end + end + +end diff --git a/ruby/lib/jam_ruby/models/affiliate_partner.rb b/ruby/lib/jam_ruby/models/affiliate_partner.rb index 6df9dc1a2..4408ffa42 100644 --- a/ruby/lib/jam_ruby/models/affiliate_partner.rb +++ b/ruby/lib/jam_ruby/models/affiliate_partner.rb @@ -1,25 +1,75 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base - belongs_to :partner_user, :class_name => "JamRuby::User", :foreign_key => :partner_user_id - has_many :user_referrals, :class_name => "JamRuby::User", :foreign_key => :affiliate_referral_id + self.table_name = 'affiliate_partners' + belongs_to :partner_user, :class_name => "JamRuby::User", :foreign_key => :partner_user_id, inverse_of: :affiliate_partner + has_many :user_referrals, :class_name => "JamRuby::User", :foreign_key => :affiliate_referral_id + belongs_to :affiliate_legalese, :class_name => "JamRuby::AffiliateLegalese", :foreign_key => :legalese_id + has_many :sale_line_items, :class_name => 'JamRuby::SaleLineItem', foreign_key: :affiliate_referral_id + has_many :quarters, :class_name => 'JamRuby::AffiliateQuarterlyPayment', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner + has_many :months, :class_name => 'JamRuby::AffiliateMonthlyPayment', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner + has_many :traffic_totals, :class_name => 'JamRuby::AffiliateTrafficTotal', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner + has_many :visits, :class_name => 'JamRuby::AffiliateReferralVisit', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner attr_accessible :partner_name, :partner_code, :partner_user_id + ENTITY_TYPES = %w{ Individual Sole\ Proprietor Limited\ Liability\ Company\ (LLC) Partnership Trust/Estate S\ Corporation C\ Corporation Other } + + KEY_ADDR1 = 'address1' + KEY_ADDR2 = 'address2' + KEY_CITY = 'city' + KEY_STATE = 'state' + KEY_POSTAL = 'postal_code' + KEY_COUNTRY = 'country' + + # ten dollars in cents + PAY_THRESHOLD = 10 * 100 + + AFFILIATE_PARAMS="utm_source=affiliate&utm_medium=affiliate&utm_campaign=2015-affiliate-custom&affiliate=" + + ADDRESS_SCHEMA = { + KEY_ADDR1 => '', + KEY_ADDR2 => '', + KEY_CITY => '', + KEY_STATE => '', + KEY_POSTAL => '', + KEY_COUNTRY => '', + } + PARAM_REFERRAL = :ref PARAM_COOKIE = :affiliate_ref PARTNER_CODE_REGEX = /^[#{Regexp.escape('abcdefghijklmnopqrstuvwxyz0123456789-._+,')}]+{2,128}$/i - validates :user_email, format: {with: JamRuby::User::VALID_EMAIL_REGEX}, :if => :user_email - validates :partner_name, presence: true - validates :partner_code, presence: true, format: { with: PARTNER_CODE_REGEX } - validates :partner_user, presence: true + #validates :user_email, format: {with: JamRuby::User::VALID_EMAIL_REGEX}, :if => :user_email + #validates :partner_code, format: { with: PARTNER_CODE_REGEX }, :allow_blank => true + validates :entity_type, inclusion: {in: ENTITY_TYPES, message: "invalid entity type"} + serialize :address, JSON + + before_save do |record| + record.address ||= ADDRESS_SCHEMA.clone + record.entity_type ||= ENTITY_TYPES.first + end + + # used by admin def self.create_with_params(params={}) + raise 'not supported' oo = self.new oo.partner_name = params[:partner_name].try(:strip) oo.partner_code = params[:partner_code].try(:strip).try(:downcase) oo.partner_user = User.where(:email => params[:user_email].try(:strip)).limit(1).first oo.partner_user_id = oo.partner_user.try(:id) + oo.entity_type = params[:entity_type] || ENTITY_TYPES.first + oo.save + oo + end + + # used by web + def self.create_with_web_params(user, params={}) + oo = self.new + oo.partner_name = params[:partner_name].try(:strip) + oo.partner_user = user if user # user is not required + oo.entity_type = params[:entity_type] || ENTITY_TYPES.first + oo.signed_at = Time.now oo.save oo end @@ -29,16 +79,394 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base end def self.is_code?(code) - self.where(:partner_code => code).limit(1).pluck(:id).present? + self.where(:partner_code => code).limit(1).pluck(:id).present? end def referrals_by_date by_date = User.where(:affiliate_referral_id => self.id) - .group('DATE(created_at)') - .having("COUNT(*) > 0") - .order('date_created_at DESC') - .count + .group('DATE(created_at)') + .having("COUNT(*) > 0") + .order('date_created_at DESC') + .count block_given? ? yield(by_date) : by_date end + def signed_legalese(legalese) + self.affiliate_legalese = legalese + self.signed_at = Time.now + save! + end + + def update_address_value(key, val) + self.address[key] = val + self.update_attribute(:address, self.address) + end + + def address_value(key) + self.address[key] + end + + def created_within_affiliate_window(user, sale_time) + sale_time - user.created_at < 2.years + end + + def should_attribute_sale?(shopping_cart) + if shopping_cart.is_jam_track? + if created_within_affiliate_window(shopping_cart.user, Time.now) + product_info = shopping_cart.product_info + # subtract the total quantity from the freebie quantity, to see how much we should attribute to them + real_quantity = product_info[:quantity].to_i - product_info[:marked_for_redeem].to_i + {fee_in_cents: real_quantity * 20} + else + false + end + else + raise 'shopping cart type not implemented yet' + end + end + + def cumulative_earnings_in_dollars + cumulative_earnings_in_cents.to_f / 100.to_f + end + + def self.quarter_info(date) + + year = date.year + + # which quarter? + quarter = -1 + if date.month >= 1 && date.month <= 3 + quarter = 0 + elsif date.month >= 4 && date.month <= 6 + quarter = 1 + elsif date.month >= 7 && date.month <= 9 + quarter = 2 + elsif date.month >= 10 && date.month <= 12 + quarter = 3 + end + + raise 'quarter should never be -1' if quarter == -1 + + previous_quarter = quarter - 1 + previous_year = date.year + if previous_quarter == -1 + previous_quarter = 3 + previous_year = year - 1 + end + + raise 'previous quarter should never be -1' if previous_quarter == -1 + + {year: year, quarter: quarter, previous_quarter: previous_quarter, previous_year: previous_year} + end + + def self.did_quarter_elapse?(quarter_info, last_tallied_info) + if last_tallied_info.nil? + true + else + quarter_info == last_tallied_info + end + end + + # meant to be run regularly; this routine will make summarized counts in the + # AffiliateQuarterlyPayment table + # AffiliatePartner.cumulative_earnings_in_cents, AffiliatePartner.referral_user_count + def self.tally_up(day) + + AffiliatePartner.transaction do + quarter_info = quarter_info(day) + last_tallied_info = quarter_info(GenericState.affiliate_tallied_at) if GenericState.affiliate_tallied_at + + quarter_elapsed = did_quarter_elapse?(quarter_info, last_tallied_info) + + if quarter_elapsed + tally_monthly_payments(quarter_info[:previous_year], quarter_info[:previous_quarter]) + tally_quarterly_payments(quarter_info[:previous_year], quarter_info[:previous_quarter]) + end + tally_monthly_payments(quarter_info[:year], quarter_info[:quarter]) + tally_quarterly_payments(quarter_info[:year], quarter_info[:quarter]) + + tally_traffic_totals(GenericState.affiliate_tallied_at, day) + + tally_partner_totals + + state = GenericState.singleton + state.affiliate_tallied_at = day + state.save! + end + + end + + # this just makes sure that the quarter rows exist before later manipulations with UPDATEs + def self.ensure_quarters_exist(year, quarter) + + sql = %{ + INSERT INTO affiliate_quarterly_payments (quarter, year, affiliate_partner_id) + (SELECT #{quarter}, #{year}, affiliate_partners.id FROM affiliate_partners WHERE affiliate_partners.partner_user_id IS NOT NULL AND affiliate_partners.id NOT IN + (SELECT affiliate_partner_id FROM affiliate_quarterly_payments WHERE year = #{year} AND quarter = #{quarter})) + } + + ActiveRecord::Base.connection.execute(sql) + end + + # this just makes sure that the quarter rows exist before later manipulations with UPDATEs + def self.ensure_months_exist(year, quarter) + + months = [1, 2, 3].collect! { |i| quarter * 3 + i } + + months.each do |month| + sql = %{ + INSERT INTO affiliate_monthly_payments (month, year, affiliate_partner_id) + (SELECT #{month}, #{year}, affiliate_partners.id FROM affiliate_partners WHERE affiliate_partners.partner_user_id IS NOT NULL AND affiliate_partners.id NOT IN + (SELECT affiliate_partner_id FROM affiliate_monthly_payments WHERE year = #{year} AND month = #{month})) + } + + ActiveRecord::Base.connection.execute(sql) + end + end + + + def self.sale_items_subquery(start_date, end_date, table_name) + %{ + FROM sale_line_items + WHERE + (DATE(sale_line_items.created_at) >= DATE('#{start_date}') AND DATE(sale_line_items.created_at) <= DATE('#{end_date}')) + AND + sale_line_items.affiliate_referral_id = #{table_name}.affiliate_partner_id + } + end + + def self.sale_items_refunded_subquery(start_date, end_date, table_name) + %{ + FROM sale_line_items + WHERE + (DATE(sale_line_items.affiliate_refunded_at) >= DATE('#{start_date}') AND DATE(sale_line_items.affiliate_refunded_at) <= DATE('#{end_date}')) + AND + sale_line_items.affiliate_referral_id = #{table_name}.affiliate_partner_id + AND + sale_line_items.affiliate_refunded = TRUE + } + end + # total up quarters by looking in sale_line_items for items that are marked as having a affiliate_referral_id + # don't forget to substract any sale_line_items that have a affiliate_refunded = TRUE + def self.total_months(year, quarter) + + months = [1, 2, 3].collect! { |i| quarter * 3 + i } + + + months.each do |month| + + start_date, end_date = boundary_dates_for_month(year, month) + + sql = %{ + UPDATE affiliate_monthly_payments + SET + last_updated = NOW(), + jamtracks_sold = + COALESCE( + (SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) + #{sale_items_subquery(start_date, end_date, 'affiliate_monthly_payments')} + ), 0) + + + COALESCE( + (SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) + #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_monthly_payments')} + ), 0), + due_amount_in_cents = + COALESCE( + (SELECT SUM(affiliate_referral_fee_in_cents) + #{sale_items_subquery(start_date, end_date, 'affiliate_monthly_payments')} + ), 0) + + + COALESCE( + (SELECT -SUM(affiliate_referral_fee_in_cents) + #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_monthly_payments')} + ), 0) + + WHERE closed = FALSE AND year = #{year} AND month = #{month} + } + + ActiveRecord::Base.connection.execute(sql) + end + end + + # close any quarters that are done, so we don't manipulate them again + def self.close_months(year, quarter) + # close any quarters that occurred before this quarter + month = quarter * 3 + 1 + + sql = %{ + UPDATE affiliate_monthly_payments + SET + closed = TRUE, closed_at = NOW() + WHERE year < #{year} OR month < #{month} + } + + ActiveRecord::Base.connection.execute(sql) + + end + + # total up quarters by looking in sale_line_items for items that are marked as having a affiliate_referral_id + # don't forget to substract any sale_line_items that have a affiliate_refunded = TRUE + def self.total_quarters(year, quarter) + start_date, end_date = boundary_dates(year, quarter) + + sql = %{ + UPDATE affiliate_quarterly_payments + SET + last_updated = NOW(), + jamtracks_sold = + COALESCE( + (SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) + #{sale_items_subquery(start_date, end_date, 'affiliate_quarterly_payments')} + ), 0) + + + COALESCE( + (SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) + #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_quarterly_payments')} + ), 0), + due_amount_in_cents = + COALESCE( + (SELECT SUM(affiliate_referral_fee_in_cents) + #{sale_items_subquery(start_date, end_date, 'affiliate_quarterly_payments')} + ), 0) + + + COALESCE( + (SELECT -SUM(affiliate_referral_fee_in_cents) + #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_quarterly_payments')} + ), 0) + + WHERE closed = FALSE AND paid = FALSE AND year = #{year} AND quarter = #{quarter} + } + + ActiveRecord::Base.connection.execute(sql) + end + + # close any quarters that are done, so we don't manipulate them again + def self.close_quarters(year, quarter) + # close any quarters that occurred before this quarter + sql = %{ + UPDATE affiliate_quarterly_payments + SET + closed = TRUE, closed_at = NOW() + WHERE year < #{year} OR quarter < #{quarter} + } + + ActiveRecord::Base.connection.execute(sql) + end + + def self.tally_quarterly_payments(year, quarter) + ensure_quarters_exist(year, quarter) + + total_quarters(year, quarter) + + close_quarters(year, quarter) + end + + + def self.tally_monthly_payments(year, quarter) + ensure_months_exist(year, quarter) + + total_months(year, quarter) + + close_months(year, quarter) + end + + def self.tally_partner_totals + sql = %{ + UPDATE affiliate_partners SET + referral_user_count = (SELECT count(*) FROM users WHERE affiliate_partners.id = users.affiliate_referral_id), + cumulative_earnings_in_cents = (SELECT COALESCE(SUM(due_amount_in_cents), 0) FROM affiliate_quarterly_payments AS aqp WHERE aqp.affiliate_partner_id = affiliate_partners.id AND closed = TRUE and paid = TRUE) + } + ActiveRecord::Base.connection.execute(sql) + end + + def self.tally_traffic_totals(last_tallied_at, target_day) + + if last_tallied_at + start_date = last_tallied_at.to_date + end_date = target_day.to_date + else + start_date = target_day.to_date - 1 + end_date = target_day.to_date + end + + if start_date == end_date + return + end + + sql = %{ + INSERT INTO affiliate_traffic_totals(SELECT day, 0, 0, ap.id FROM affiliate_partners AS ap CROSS JOIN (select (generate_series('#{start_date}', '#{end_date - 1}', '1 day'::interval))::date as day) AS lurp) + } + ActiveRecord::Base.connection.execute(sql) + + sql = %{ + UPDATE affiliate_traffic_totals traffic SET visits = COALESCE((SELECT COALESCE(count(affiliate_partner_id), 0) FROM affiliate_referral_visits v WHERE DATE(v.created_at) >= DATE('#{start_date}') AND DATE(v.created_at) < DATE('#{end_date}') AND v.created_at::date = traffic.day AND v.affiliate_partner_id = traffic.affiliate_partner_id GROUP BY affiliate_partner_id, v.created_at::date ), 0) WHERE traffic.day >= DATE('#{start_date}') AND traffic.day < DATE('#{end_date}') + } + ActiveRecord::Base.connection.execute(sql) + + sql = %{ + UPDATE affiliate_traffic_totals traffic SET signups = COALESCE((SELECT COALESCE(count(v.id), 0) FROM users v WHERE DATE(v.created_at) >= DATE('#{start_date}') AND DATE(v.created_at) < DATE('#{end_date}') AND v.created_at::date = traffic.day AND v.affiliate_referral_id = traffic.affiliate_partner_id GROUP BY affiliate_referral_id, v.created_at::date ), 0) WHERE traffic.day >= DATE('#{start_date}') AND traffic.day < DATE('#{end_date}') + } + ActiveRecord::Base.connection.execute(sql) + end + + def self.boundary_dates(year, quarter) + if quarter == 0 + [Date.new(year, 1, 1), Date.new(year, 3, 31)] + elsif quarter == 1 + [Date.new(year, 4, 1), Date.new(year, 6, 30)] + elsif quarter == 2 + [Date.new(year, 7, 1), Date.new(year, 9, 30)] + elsif quarter == 3 + [Date.new(year, 10, 1), Date.new(year, 12, 31)] + else + raise "invalid quarter #{quarter}" + end + end + + # 1-based month + def self.boundary_dates_for_month(year, month) + [Date.new(year, month, 1), Date.civil(year, month, -1)] + end + + # Finds all affiliates that need to be paid + def self.unpaid + + joins(:quarters) + .where('affiliate_quarterly_payments.paid = false').where('affiliate_quarterly_payments.closed = true') + .group('affiliate_partners.id') + .having('sum(due_amount_in_cents) >= ?', PAY_THRESHOLD) + .order('sum(due_amount_in_cents) DESC') + + end + + # does this one affiliate need to be paid? + def unpaid + due_amount_in_cents > PAY_THRESHOLD + end + + # admin function: mark the affiliate paid + def mark_paid + if unpaid + transaction do + now = Time.now + quarters.where(paid:false, closed:true).update_all(paid:true, paid_at: now) + self.last_paid_at = now + self.save! + end + end + end + + # how much is this affiliate due? + def due_amount_in_cents + total_in_cents = 0 + quarters.where(paid:false, closed:true).each do |quarter| + total_in_cents = total_in_cents + quarter.due_amount_in_cents + end + total_in_cents + end + + def affiliate_query_params + AffiliatePartner::AFFILIATE_PARAMS + self.id.to_s + end end diff --git a/ruby/lib/jam_ruby/models/affiliate_payment.rb b/ruby/lib/jam_ruby/models/affiliate_payment.rb new file mode 100644 index 000000000..1aa29f1c8 --- /dev/null +++ b/ruby/lib/jam_ruby/models/affiliate_payment.rb @@ -0,0 +1,49 @@ +module JamRuby + class AffiliatePayment < ActiveRecord::Base + + belongs_to :affiliate_monthly_payment + belongs_to :affiliate_quarterly_payment + + def self.index(user, options) + + unless user.affiliate_partner + return [[], nil] + end + + affiliate_partner_id = user.affiliate_partner.id + + + page = options[:page].to_i + per_page = options[:per_page].to_i + + if page == 0 + page = 1 + end + + if per_page == 0 + per_page = 50 + end + + start = (page -1 ) * per_page + limit = per_page + + + query = AffiliatePayment + .includes(affiliate_quarterly_payment: [], affiliate_monthly_payment:[]) + .where(affiliate_partner_id: affiliate_partner_id) + .where("(payment_type='quarterly' AND closed = true) OR payment_type='monthly'") + .where('(paid = TRUE or due_amount_in_cents < 10000 or paid is NULL)') + .paginate(:page => page, :per_page => limit) + .order('year ASC, time_sort ASC, payment_type ASC') + + if query.length == 0 + [query, nil] + elsif query.length < limit + [query, nil] + else + [query, start + limit] + end + end + end +end + diff --git a/ruby/lib/jam_ruby/models/affiliate_quarterly_payment.rb b/ruby/lib/jam_ruby/models/affiliate_quarterly_payment.rb new file mode 100644 index 000000000..caf9eced7 --- /dev/null +++ b/ruby/lib/jam_ruby/models/affiliate_quarterly_payment.rb @@ -0,0 +1,41 @@ +class JamRuby::AffiliateQuarterlyPayment < ActiveRecord::Base + + belongs_to :affiliate_partner, class_name: 'JamRuby::AffiliatePartner', inverse_of: :quarters + + + def self.index(user, options) + unless user.affiliate_partner + return [[], nil] + end + + page = options[:page].to_i + per_page = options[:per_page].to_i + + if page == 0 + page = 1 + end + + if per_page == 0 + per_page = 50 + end + + start = (page -1 ) * per_page + limit = per_page + + query = AffiliateQuarterlyPayment + .paginate(page: page, per_page: per_page) + .where(affiliate_partner_id: user.affiliate_partner.id) + .where(closed:true) + .where(paid:true) + .order('year ASC, quarter ASC') + + if query.length == 0 + [query, nil] + elsif query.length < limit + [query, nil] + else + [query, start + limit] + end + end + +end diff --git a/ruby/lib/jam_ruby/models/affiliate_referral_visit.rb b/ruby/lib/jam_ruby/models/affiliate_referral_visit.rb new file mode 100644 index 000000000..cf9e9fbc0 --- /dev/null +++ b/ruby/lib/jam_ruby/models/affiliate_referral_visit.rb @@ -0,0 +1,23 @@ +class JamRuby::AffiliateReferralVisit < ActiveRecord::Base + + belongs_to :affiliate_partner, class_name: 'JamRuby::AffiliatePartner', inverse_of: :visits + + validates :affiliate_partner_id, numericality: {only_integer: true}, :allow_nil => true + validates :visited_url, length: {maximum: 1000} + validates :referral_url, length: {maximum: 1000} + validates :ip_address, presence: true, length: {maximum:1000} + validates :first_visit, inclusion: {in: [true, false]} + validates :user_id, length: {maximum:64} + + def self.track(options = {}) + visit = AffiliateReferralVisit.new + visit.affiliate_partner_id = options[:affiliate_id] + visit.ip_address = options[:remote_ip] + visit.visited_url = options[:visited_url] + visit.referral_url = options[:referral_url] + visit.first_visit = options[:visited].nil? + visit.user_id = options[:current_user].id if options[:current_user] + visit.save + visit + end +end diff --git a/ruby/lib/jam_ruby/models/affiliate_traffic_total.rb b/ruby/lib/jam_ruby/models/affiliate_traffic_total.rb new file mode 100644 index 000000000..c85fa4fb5 --- /dev/null +++ b/ruby/lib/jam_ruby/models/affiliate_traffic_total.rb @@ -0,0 +1,39 @@ +class JamRuby::AffiliateTrafficTotal < ActiveRecord::Base + + belongs_to :affiliate_partner, class_name: 'JamRuby::AffiliatePartner', inverse_of: :traffic_totals + + + def self.index(user, options) + unless user.affiliate_partner + return [[], nil] + end + + page = options[:page].to_i + per_page = options[:per_page].to_i + + if page == 0 + page = 1 + end + + if per_page == 0 + per_page = 50 + end + + start = (page -1 ) * per_page + limit = per_page + + query = AffiliateTrafficTotal + .paginate(page: page, per_page: per_page) + .where(affiliate_partner_id: user.affiliate_partner.id) + .where('visits != 0 OR signups != 0') + .order('day ASC') + + if query.length == 0 + [query, nil] + elsif query.length < limit + [query, nil] + else + [query, start + limit] + end + end +end diff --git a/ruby/lib/jam_ruby/models/anonymous_user.rb b/ruby/lib/jam_ruby/models/anonymous_user.rb index 0a843185a..24bc2b104 100644 --- a/ruby/lib/jam_ruby/models/anonymous_user.rb +++ b/ruby/lib/jam_ruby/models/anonymous_user.rb @@ -4,14 +4,15 @@ module JamRuby class AnonymousUser - attr_accessor :id + attr_accessor :id, :cookies - def initialize(id) + def initialize(id, cookies) @id = id + @cookies = cookies end def shopping_carts - ShoppingCart.where(anonymous_user_id: @id) + ShoppingCart.where(anonymous_user_id: @id).order('created_at DESC') end def destroy_all_shopping_carts @@ -23,7 +24,11 @@ module JamRuby end def has_redeemable_jamtrack - true + APP_CONFIG.one_free_jamtrack_per_user && !@cookies[:redeemed_jamtrack] + end + + def signup_hint + SignupHint.where(anonymous_user_id: @id).where('expires_at > ?', Time.now).first end end end diff --git a/ruby/lib/jam_ruby/models/artifact_update.rb b/ruby/lib/jam_ruby/models/artifact_update.rb index d79d854d9..9b671df91 100644 --- a/ruby/lib/jam_ruby/models/artifact_update.rb +++ b/ruby/lib/jam_ruby/models/artifact_update.rb @@ -44,7 +44,7 @@ module JamRuby # this is basically a dev-time only path of code; we store real artifacts in s3 url = APP_CONFIG.jam_admin_root_url + self.uri.url else - url = "http://#{APP_CONFIG.cloudfront_host}/#{self.uri.store_dir}/#{self[:uri]}" + url = "https://#{APP_CONFIG.cloudfront_host}/#{self.uri.store_dir}/#{self[:uri]}" #url = self.uri.url.gsub(APP_CONFIG.aws_fullhost, APP_CONFIG.cloudfront_host) end diff --git a/ruby/lib/jam_ruby/models/band.rb b/ruby/lib/jam_ruby/models/band.rb index cfd554d2b..f64bc47d2 100644 --- a/ruby/lib/jam_ruby/models/band.rb +++ b/ruby/lib/jam_ruby/models/band.rb @@ -163,14 +163,14 @@ module JamRuby # ensure person creating this Band is a Musician unless user.musician? - raise PermissionError, "must be a musician" + raise JamPermissionError, "must be a musician" end band = id.blank? ? Band.new : Band.find(id) # ensure user updating Band details is a Band member unless band.new_record? || band.users.exists?(user) - raise PermissionError, ValidationMessages::USER_NOT_BAND_MEMBER_VALIDATION_ERROR + raise JamPermissionError, ValidationMessages::USER_NOT_BAND_MEMBER_VALIDATION_ERROR end band.name = params[:name] if params.has_key?(:name) @@ -224,8 +224,8 @@ module JamRuby :cropped_s3_path_photo => cropped_s3_path, :cropped_large_s3_path_photo => cropped_large_s3_path, :crop_selection_photo => crop_selection, - :photo_url => S3Util.url(aws_bucket, escape_filename(cropped_s3_path), :secure => false), - :large_photo_url => S3Util.url(aws_bucket, escape_filename(cropped_large_s3_path), :secure => false)) + :photo_url => S3Util.url(aws_bucket, escape_filename(cropped_s3_path), :secure => true), + :large_photo_url => S3Util.url(aws_bucket, escape_filename(cropped_large_s3_path), :secure => true)) end def delete_photo(aws_bucket) diff --git a/ruby/lib/jam_ruby/models/claimed_recording.rb b/ruby/lib/jam_ruby/models/claimed_recording.rb index 88898cb89..18dd11898 100644 --- a/ruby/lib/jam_ruby/models/claimed_recording.rb +++ b/ruby/lib/jam_ruby/models/claimed_recording.rb @@ -40,7 +40,7 @@ module JamRuby # params is a hash, and everything is optional def update_fields(user, params) if user != self.user - raise PermissionError, "user doesn't own claimed_recording" + raise JamPermissionError, "user doesn't own claimed_recording" end self.name = params[:name] @@ -52,7 +52,7 @@ module JamRuby def discard(user) if user != self.user - raise PermissionError, "user doesn't own claimed_recording" + raise JamPermissionError, "user doesn't own claimed_recording" end ClaimedRecording.where(:id => id).update_all(:discarded => true ) @@ -95,11 +95,11 @@ module JamRuby target_user = params[:user] - raise PermissionError, "must specify current user" unless user + raise JamPermissionError, "must specify current user" unless user raise "user must be specified" unless target_user if target_user != user.id - raise PermissionError, "unable to view another user's favorites" + raise JamPermissionError, "unable to view another user's favorites" end query = ClaimedRecording.limit(limit).order('created_at DESC').offset(start) diff --git a/ruby/lib/jam_ruby/models/connection.rb b/ruby/lib/jam_ruby/models/connection.rb index 26dd417fe..5a5378b91 100644 --- a/ruby/lib/jam_ruby/models/connection.rb +++ b/ruby/lib/jam_ruby/models/connection.rb @@ -24,11 +24,12 @@ module JamRuby validates :metronome_open, :inclusion => {:in => [true, false]} validates :as_musician, :inclusion => {:in => [true, false, nil]} validates :client_type, :inclusion => {:in => CLIENT_TYPES} - validates_numericality_of :last_jam_audio_latency, greater_than:0, :allow_nil => true + validates_numericality_of :last_jam_audio_latency, greater_than: 0, :allow_nil => true validate :can_join_music_session, :if => :joining_session? validate :user_or_latency_tester_present - after_save :require_at_least_one_track_when_in_session, :if => :joining_session? + # this is no longer required with the new no-input profile + #after_save :require_at_least_one_track_when_in_session, :if => :joining_session? after_create :did_create after_save :report_add_participant @@ -62,11 +63,11 @@ module JamRuby def state_message case self.aasm_state.to_sym when CONNECT_STATE - 'Connected' - when STALE_STATE - 'Stale' + 'Connected' + when STALE_STATE + 'Stale' else - 'Idle' + 'Idle' end end @@ -85,7 +86,7 @@ module JamRuby def joining_session? joining_session end - + def can_join_music_session # puts "can_join_music_session: #{music_session_id} was #{music_session_id_was}" if music_session_id_changed? @@ -183,8 +184,8 @@ module JamRuby end def associate_tracks(tracks) + self.tracks.clear() unless tracks.nil? - self.tracks.clear() tracks.each do |track| t = Track.new t.instrument = Instrument.find(track["instrument_id"]) diff --git a/ruby/lib/jam_ruby/models/fingerprint_whitelist.rb b/ruby/lib/jam_ruby/models/fingerprint_whitelist.rb new file mode 100644 index 000000000..88f015aa3 --- /dev/null +++ b/ruby/lib/jam_ruby/models/fingerprint_whitelist.rb @@ -0,0 +1,17 @@ +module JamRuby + class FingerprintWhitelist < ActiveRecord::Base + + @@log = Logging.logger[FingerprintWhitelist] + + validates :fingerprint, presence: true, uniqueness: true + has_many :machine_fingerprint, class_name: 'JamRuby::MachineFingerprint', foreign_key: :fingerprint + + def admin_url + APP_CONFIG.admin_root_url + "/admin/fingerprint_whitelists/" + id + end + + def to_s + "#{fingerprint}" + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/fraud_alert.rb b/ruby/lib/jam_ruby/models/fraud_alert.rb new file mode 100644 index 000000000..344eb7ddc --- /dev/null +++ b/ruby/lib/jam_ruby/models/fraud_alert.rb @@ -0,0 +1,26 @@ +module JamRuby + class FraudAlert < ActiveRecord::Base + + @@log = Logging.logger[MachineExtra] + + belongs_to :machine_fingerprint, :class_name => "JamRuby::MachineFingerprint" + belongs_to :user, :class_name => "JamRuby::User" + + + def self.create(machine_fingerprint, user) + fraud = FraudAlert.new + fraud.machine_fingerprint = machine_fingerprint + fraud.user = user + fraud.save + + unless fraud.save + @@log.error("unable to create fraud: #{fraud.errors.inspect}") + end + fraud + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/fraud_alerts/" + id + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/generic_state.rb b/ruby/lib/jam_ruby/models/generic_state.rb index d6c0bc687..6ed4ec7f4 100644 --- a/ruby/lib/jam_ruby/models/generic_state.rb +++ b/ruby/lib/jam_ruby/models/generic_state.rb @@ -23,9 +23,14 @@ module JamRuby (database_environment == 'development' && Environment.mode == 'development') end + def self.affiliate_tallied_at + GenericState.singleton.affiliate_tallied_at + end + def self.singleton GenericState.find('default') end + end end diff --git a/ruby/lib/jam_ruby/models/icecast_mount_template.rb b/ruby/lib/jam_ruby/models/icecast_mount_template.rb index c93dc1c8a..c6094669a 100644 --- a/ruby/lib/jam_ruby/models/icecast_mount_template.rb +++ b/ruby/lib/jam_ruby/models/icecast_mount_template.rb @@ -54,7 +54,7 @@ module JamRuby mount.source_pass = APP_CONFIG.icecast_hardcoded_source_password || SecureRandom.urlsafe_base64 mount.stream_name = "JamKazam music session created by #{music_session.creator.name}" mount.stream_description = music_session.description - mount.stream_url = "http://www.jamkazam.com" ## TODO/XXX, the jamkazam url should be the page hosting the widget + mount.stream_url = "https://www.jamkazam.com" ## TODO/XXX, the jamkazam url should be the page hosting the widget mount.genre = music_session.genre.description mount end diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb index a4d293276..8ab120c62 100644 --- a/ruby/lib/jam_ruby/models/jam_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -17,7 +17,7 @@ module JamRuby :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, :plan_code, :initial_play_silence, :jam_track_tracks_attributes, - :jam_track_tap_ins_attributes, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, as: :admin + :jam_track_tap_ins_attributes, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, :duration, as: :admin validates :name, presence: true, uniqueness: true, length: {maximum: 200} validates :plan_code, presence: true, uniqueness: true, length: {maximum: 50 } @@ -60,7 +60,10 @@ module JamRuby # has_many :plays, :class_name => "JamRuby::PlayablePlay", :foreign_key => :jam_track_id, :dependent => :destroy # VRFS-2916 jam_tracks.id is varchar: ADD has_many :plays, :class_name => "JamRuby::PlayablePlay", :as => :playable, :dependent => :destroy - + + # when we know what JamTrack this refund is related to, these are associated + belongs_to :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook' + accepts_nested_attributes_for :jam_track_tracks, allow_destroy: true accepts_nested_attributes_for :jam_track_tap_ins, allow_destroy: true @@ -100,6 +103,7 @@ module JamRuby warnings << 'POSITIONS' if duplicate_positions? warnings << 'PREVIEWS'if missing_previews? warnings << 'DURATION' if duration.nil? + warnings << 'JMEP' if jmep_json.blank? warnings.join(',') end @@ -112,6 +116,7 @@ module JamRuby def all_artists JamTrack.select("original_artist"). group("original_artist"). + order('original_artist'). collect{|jam_track|jam_track.original_artist} end @@ -161,16 +166,75 @@ module JamRuby query = query.where("original_artist=?", options[:artist]) end - if options[:group_artist] - query = query.group("original_artist") + if options[:id].present? + query = query.where("jam_tracks.id=?", options[:id]) end - + + if options[:group_artist] + query = query.select("original_artist, array_agg(jam_tracks.id) AS id, MIN(name) AS name, MIN(description) AS description, MIN(recording_type) AS recording_type, MIN(original_artist) AS original_artist, MIN(songwriter) AS songwriter, MIN(publisher) AS publisher, MIN(sales_region) AS sales_region, MIN(price) AS price, MIN(version) AS version, MIN(genre_id) AS genre_id") + query = query.group("original_artist") + query = query.order('jam_tracks.original_artist') + else + query = query.group("jam_tracks.id") + query = query.order('jam_tracks.original_artist, jam_tracks.name') + end + + query = query.where("jam_tracks.status = ?", 'Production') unless user.admin + query = query.where("jam_tracks.genre_id = '#{options[:genre]}'") unless options[:genre].blank? + query = query.where("jam_track_tracks.instrument_id = '#{options[:instrument]}' and jam_track_tracks.track_type != 'Master'") unless options[:instrument].blank? + query = query.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank? + + + if query.length == 0 + [query, nil] + elsif query.length < limit + [query, nil] + else + [query, start + limit] + end + end + + + # provides artist names and how many jamtracks are available for each + def artist_index(options, user) + if options[:page] + page = options[:page].to_i + per_page = options[:per_page].to_i + + if per_page == 0 + # try and see if limit was specified + limit = options[:limit] + limit ||= 100 + limit = limit.to_i + else + limit = per_page + end + + start = (page -1 )* per_page + limit = per_page + else + limit = options[:limit] + limit ||= 100 + limit = limit.to_i + + start = options[:start].presence + start = start.to_i || 0 + + page = 1 + start/limit + per_page = limit + end + + + query = JamTrack.paginate(page: page, per_page: per_page) + query = query.select("original_artist, count(original_artist) AS song_count") + query = query.group("original_artist") + query = query.order('jam_tracks.original_artist') + query = query.where("jam_tracks.status = ?", 'Production') unless user.admin 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") - query = query.order('jam_tracks.name') + if query.length == 0 [query, nil] @@ -187,6 +251,10 @@ module JamRuby JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Master').first end + def stem_tracks + JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Track') + end + def can_download?(user) owners.include?(user) end @@ -194,5 +262,11 @@ module JamRuby def right_for_user(user) jam_track_rights.where("user_id=?", user).first end + + def short_plan_code + prefix = 'jamtrack-' + plan_code[prefix.length..-1] + end + end end diff --git a/ruby/lib/jam_ruby/models/jam_track_right.rb b/ruby/lib/jam_ruby/models/jam_track_right.rb index f5abb1353..f0749dc94 100644 --- a/ruby/lib/jam_ruby/models/jam_track_right.rb +++ b/ruby/lib/jam_ruby/models/jam_track_right.rb @@ -3,21 +3,24 @@ module JamRuby # describes what users have rights to which tracks class JamTrackRight < ActiveRecord::Base include JamRuby::S3ManagerMixin - attr_accessible :user, :jam_track, :user_id, :jam_track_id, :download_count + + @@log = Logging.logger[JamTrackRight] + + attr_accessible :user, :jam_track, :user_id, :jam_track_id, :download_count attr_accessible :user_id, :jam_track_id, as: :admin attr_accessible :url_48, :md5_48, :length_48, :url_44, :md5_44, :length_44 - belongs_to :user, class_name: "JamRuby::User" # the owner, or purchaser of the jam_track + 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 :is_test_purchase, inclusion: {in: [true, false]} + validates :user, presence: true + validates :jam_track, presence: true + validates :is_test_purchase, inclusion: {in: [true, false]} validate :verify_download_count after_save :after_save - validates_uniqueness_of :user_id, scope: :jam_track_id - + validates_uniqueness_of :user_id, scope: :jam_track_id + # Uploads the JKZ: mount_uploader :url_48, JamTrackRightUploader mount_uploader :url_44, JamTrackRightUploader @@ -29,7 +32,7 @@ module JamRuby # try to catch major transitions: # if just queue time changes, start time changes, or signed time changes, send out a notice - if signing_queued_at_was != signing_queued_at || signing_started_at_was != signing_started_at || last_signed_at_was != last_signed_at + if signing_queued_at_was != signing_queued_at || signing_started_at_48_was != signing_started_at_48 || signing_started_at_44_was != signing_started_at_44 || last_signed_at_was != last_signed_at || current_packaging_step != current_packaging_step_was || packaging_steps != packaging_steps_was SubscriptionMessage.jam_track_signing_job_change(self) end end @@ -39,10 +42,10 @@ module JamRuby end # create name of the file - def filename - "#{jam_track.name}.jkz" + def filename(bitrate) + "#{jam_track.name}-#{bitrate == :url_48 ? '48' : '44'}.jkz" end - + def verify_download_count if (self.download_count < 0 || self.download_count > MAX_JAM_TRACK_DOWNLOADS) && !@current_user.admin errors.add(:download_count, "must be less than or equal to #{MAX_JAM_TRACK_DOWNLOADS}") @@ -50,15 +53,21 @@ module JamRuby end def self.ready_to_clean - JamTrackRight.where("downloaded_since_sign=? AND updated_at <= ?", true, 5.minutes.ago).limit(1000) + JamTrackRight.where("downloaded_since_sign=? AND updated_at <= ?", true, 5.minutes.ago).limit(1000) end - def finish_errored(error_reason, error_detail) + def finish_errored(error_reason, error_detail, sample_rate) self.last_signed_at = Time.now self.error_count = self.error_count + 1 self.error_reason = error_reason self.error_detail = error_detail self.should_retry = self.error_count < 5 + if sample_rate == 48 + self.signing_48 = false + else + self.signing_44 = false + end + if save Notification.send_jam_track_sign_failed(self) else @@ -71,20 +80,19 @@ module JamRuby if bitrate==48 self.length_48 = length self.md5_48 = md5 + self.signed_48 = true + self.signing_48 = false else self.length_44 = length self.md5_44 = md5 + self.signed_44 = true + self.signing_44 = false end - self.signed = true self.error_count = 0 self.error_reason = nil self.error_detail = nil self.should_retry = false - if save - Notification.send_jam_track_sign_complete(self) - else - raise "Error sending notification #{self.errors}" - end + save! end # creates a short-lived URL that has access to the object. @@ -93,18 +101,18 @@ module JamRuby # but the url is short lived enough so that it wouldn't be easily shared def sign_url(expiration_time = 120, bitrate=48) field_name = (bitrate==48) ? "url_48" : "url_44" - s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => false}) + s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => true}) end def delete_s3_files - remove_url_48! - remove_url_44! + remove_url_48! + remove_url_44! end def enqueue(sample_rate=48) begin - JamTrackRight.where(:id => self.id).update_all(:signing_queued_at => Time.now, :signing_started_at => nil, :last_signed_at => nil) + JamTrackRight.where(:id => self.id).update_all(:signing_queued_at => Time.now, :signing_started_at_44 => nil, :signing_started_at_48 => nil, :last_signed_at => nil) Resque.enqueue(JamTracksBuilder, self.id, sample_rate) true rescue Exception => e @@ -116,7 +124,7 @@ module JamRuby # if the job is already signed, just queued up for signing, or currently signing, then don't enqueue... otherwise fire it off def enqueue_if_needed(sample_rate=48) - state = signing_state + state = signing_state(sample_rate) if state == 'SIGNED' || state == 'SIGNING' || state == 'QUEUED' false else @@ -129,9 +137,9 @@ module JamRuby # @return true if signed && file exists for the sample_rate specifed: def ready?(sample_rate=48) if sample_rate==48 - self.signed && self.url_48.present? && self.url_48.file.exists? + self.signed_48 && self.url_48.present? && self.url_48.file.exists? else - self.signed && self.url_44.present? && self.url_44.file.exists? + self.signed_44 && self.url_44.present? && self.url_44.file.exists? end end @@ -143,12 +151,28 @@ module JamRuby # QUEUED_TIMEOUT - the package signing job (JamTrackBuilder) was queued, but never executed # QUEUED - the package is queued to sign # QUIET - the jam_track_right exists, but no job has been kicked off; a job needs to be enqueued - def signing_state + def signing_state(sample_rate = nil) state = nil + + # if the caller did not specified sample rate, we will determine what signing state to check by looking at the most recent signing attempt + if sample_rate.nil? + # determine what package is being signed by checking the most recent signing_started at + time_48 = signing_started_at_48.to_i + time_44 = signing_started_at_44.to_i + sample_rate = time_48 > time_44 ? 48 : 44 + end + + signed = sample_rate == 48 ? signed_48 : signed_44 + signing_started_at = sample_rate == 48 ? signing_started_at_48 : signing_started_at_44 + if signed state = 'SIGNED' elsif signing_started_at - if Time.now - signing_started_at > APP_CONFIG.signing_job_run_max_time + # the maximum amount of time the packaging job can take is 10 seconds * num steps. For a 10 track song, this will be 110 seconds. It's a bit long. + signing_job_run_max_time = packaging_steps * 10 + if Time.now - signing_started_at > signing_job_run_max_time + state = 'SIGNING_TIMEOUT' + elsif Time.now - last_step_at > APP_CONFIG.signing_step_max_time state = 'SIGNING_TIMEOUT' else state = 'SIGNING' @@ -166,9 +190,18 @@ module JamRuby end state end + + def signed?(sample_rate) + sample_rate == 48 ? signed_48 : signed_44 + end + def update_download_count(count=1) self.download_count = self.download_count + count self.last_downloaded_at = Time.now + + if self.signed_44 || self.signed_48 + self.downloaded_since_sign = true + end end def self.list_keys(user, jamtracks) @@ -176,10 +209,144 @@ module JamRuby return [] end - JamTrack.select('jam_tracks.id, jam_track_rights.private_key AS private_key, jam_track_rights.id AS jam_track_right_id') + JamTrack.select('jam_tracks.id, jam_track_rights.private_key_44 AS private_key_44, jam_track_rights.private_key_48 AS private_key_48, jam_track_rights.id AS jam_track_right_id') .joins("LEFT OUTER JOIN jam_track_rights ON jam_tracks.id = jam_track_rights.jam_track_id AND jam_track_rights.user_id = '#{user.id}'") .where('jam_tracks.id IN (?)', jamtracks) end - + + def guard_against_fraud(current_user, fingerprint, remote_ip) + + if current_user.blank? + return "no user specified" + end + + # admin's get to skip fraud check + if current_user.admin + return nil + end + + if fingerprint.nil? || fingerprint.empty? + return "no fingerprint specified" + end + + all_fingerprint = fingerprint.delete(:all) + running_fingerprint = fingerprint.delete(:running) + + if all_fingerprint.blank? + return "no all fingerprint specified" + end + + if running_fingerprint.blank? + return "no running fingerprint specified" + end + + all_fingerprint_extra = fingerprint[all_fingerprint] + running_fingerprint_extra = fingerprint[running_fingerprint] + + if redeemed && !redeemed_and_fingerprinted + # if this is a free JamTrack, we need to check for fraud or accidental misuse + + # first of all, does this user have any other JamTracks aside from this one that have already been redeemed it and are marked free? + other_redeemed_freebie = JamTrackRight.where(redeemed: true).where(redeemed_and_fingerprinted: true).where('id != ?', id).where(user_id: current_user.id).first + + if other_redeemed_freebie + return "already redeemed another" + end + + if FingerprintWhitelist.select('id').find_by_fingerprint(all_fingerprint) + # we can short circuit out of the rest of the check, since this is a known bad fingerprint + @@log.debug("ignoring 'all' hash found in whitelist") + else + # can we find a jam track that belongs to someone else with the same fingerprint + conflict = MachineFingerprint.select('count(id) as count').where('user_id != ?', current_user.id).where(fingerprint: all_fingerprint).where(remote_ip: remote_ip).where('created_at > ?', APP_CONFIG.expire_fingerprint_days.days.ago).first + conflict_count = conflict['count'].to_i + + if conflict_count >= APP_CONFIG.found_conflict_count + mf = MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, all_fingerprint_extra, self) + + # record the alert + fraud = FraudAlert.create(mf, current_user) if mf.valid? + fraud_admin_url = fraud.admin_url if fraud + + + AdminMailer.alerts(subject: "'All' fingerprint collision by #{current_user.name}", + body: "Current User: #{current_user.admin_url}\n\n Fraud Alert: #{fraud_admin_url}").deliver + + # try to record the other fingerprint + mf = MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, running_fingerprint_extra, self) + + if APP_CONFIG.error_on_fraud + return "other user has 'all' fingerprint" + else + self.redeemed_and_fingerprinted = true + save! + return nil + end + + end + + end + + + if all_fingerprint != running_fingerprint + if FingerprintWhitelist.select('id').find_by_fingerprint(running_fingerprint) + # we can short circuit out of the rest of the check, since this is a known bad fingerprint + @@log.debug("ignoring 'running' hash found in whitelist") + else + + conflict = MachineFingerprint.select('count(id) as count').where('user_id != ?', current_user.id).where(fingerprint: running_fingerprint).where(remote_ip: remote_ip).where('created_at > ?', APP_CONFIG.expire_fingerprint_days.days.ago).first + conflict_count = conflict['count'].to_i + if conflict_count >= APP_CONFIG.found_conflict_count + mf = MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, running_fingerprint_extra, self) + + # record the alert + fraud = FraudAlert.create(mf, current_user) if mf.valid? + fraud_admin_url = fraud.admin_url if fraud + AdminMailer.alerts(subject: "'Running' fingerprint collision by #{current_user.name}", + body: "Current User: #{current_user.admin_url}\n\nFraud Alert: #{fraud_admin_url}").deliver\ + + # try to record the other fingerprint + mf = MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ALL, remote_ip, all_fingerprint_extra, self) + + + if APP_CONFIG.error_on_fraud + return "other user has 'running' fingerprint" + else + self.redeemed_and_fingerprinted = true + save! + return nil + end + end + end + + end + + # we made it past all checks; let's slap on the redeemed_fingerprint + self.redeemed_and_fingerprinted = true + + MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD, MachineFingerprint::PRINT_TYPE_ALL, remote_ip, all_fingerprint_extra, self) + if all_fingerprint != running_fingerprint + MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, running_fingerprint_extra, self) + end + + save! + end + + + nil + end + + def self.stats + stats = {} + + result = JamTrackRight.select('count(id) as total, count(CASE WHEN signing_44 THEN 1 ELSE NULL END) + count(CASE WHEN signing_48 THEN 1 ELSE NULL END) as signing_count, count(CASE WHEN redeemed THEN 1 ELSE NULL END) as redeem_count, count(last_downloaded_at) as redeemed_and_dl_count').where(is_test_purchase: false).first + + stats['count'] = result['total'].to_i + stats['signing_count'] = result['signing_count'].to_i + stats['redeemed_count'] = result['redeem_count'].to_i + stats['redeemed_and_dl_count'] = result['redeemed_and_dl_count'].to_i + stats['purchased_count'] = stats['count'] - stats['redeemed_count'] + stats + end end end diff --git a/ruby/lib/jam_ruby/models/jam_track_track.rb b/ruby/lib/jam_ruby/models/jam_track_track.rb index 41514b2f8..e20469076 100644 --- a/ruby/lib/jam_ruby/models/jam_track_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track_track.rb @@ -10,6 +10,7 @@ module JamRuby @@log = Logging.logger[JamTrackTrack] + before_destroy :delete_s3_files # Because JamTrackImporter imports audio files now, and because also the mere presence of this causes serious issues when updating the model (because reset of url_44 to something bogus), I've removed these #mount_uploader :url_48, JamTrackTrackUploader @@ -18,7 +19,9 @@ module JamRuby attr_accessible :jam_track_id, :track_type, :instrument, :instrument_id, :position, :part, as: :admin attr_accessible :url_44, :url_48, :md5_44, :md5_48, :length_44, :length_48, :preview_start_time_raw, as: :admin - attr_accessor :original_audio_s3_path, :skip_uploader + attr_accessor :original_audio_s3_path, :skip_uploader, :preview_generate_error + + before_destroy :delete_s3_files validates :position, presence: true, numericality: {only_integer: true}, length: {in: 1..1000} validates :part, length: {maximum: 25} @@ -57,7 +60,7 @@ module JamRuby def preview_public_url(media_type='ogg') url = media_type == 'ogg' ? self[:preview_url] : self[:preview_mp3_url] if url - s3_public_manager.public_url(url,{ :secure => false}) + s3_public_manager.public_url(url,{ :secure => true}) else nil end @@ -84,7 +87,7 @@ module JamRuby # 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, sample_rate=48) - s3_manager.sign_url(url_by_sample_rate(sample_rate), {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false}) + s3_manager.sign_url(url_by_sample_rate(sample_rate), {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => true}) end def can_download?(user) @@ -120,7 +123,99 @@ module JamRuby end end - private + def delete_s3_files + s3_manager.delete(self[:url_44]) if self[:url_44] && s3_manager.exists?(self[:url_44]) + s3_manager.delete(self[:url_48]) if self[:url_48] && s3_manager.exists?(self[:url_48]) + s3_public_manager.delete(self[:preview_url]) if self[:preview_url] && s3_public_manager.exists?(self[:preview_url]) + s3_public_manager.delete(self[:preview_mp3_url]) if self[:preview_mp3_url] && s3_public_manager.exists?(self[:preview_mp3_url]) + end + + + def generate_preview + + begin + Dir.mktmpdir do |tmp_dir| + + input = File.join(tmp_dir, 'in.ogg') + output = File.join(tmp_dir, 'out.ogg') + output_mp3 = File.join(tmp_dir, 'out.mp3') + + start = self.preview_start_time.to_f / 1000 + stop = start + 20 + + raise 'no track' unless self["url_44"] + + s3_manager.download(self.url_by_sample_rate(44), input) + + command = "sox \"#{input}\" \"#{output}\" trim #{sprintf("%.3f", start)} =#{sprintf("%.3f", stop)}" + + @@log.debug("trimming using: " + command) + + sox_output = `#{command}` + + result_code = $?.to_i + + if result_code != 0 + @@log.debug("fail #{result_code}") + @preview_generate_error = "unable to execute cut command #{sox_output}" + else + # now create mp3 off of ogg preview + + convert_mp3_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{output}\" -ab 192k \"#{output_mp3}\"" + + @@log.debug("converting to mp3 using: " + convert_mp3_cmd) + + convert_output = `#{convert_mp3_cmd}` + + result_code = $?.to_i + + if result_code != 0 + @@log.debug("fail #{result_code}") + @preview_generate_error = "unable to execute mp3 convert command #{convert_output}" + else + ogg_digest = ::Digest::MD5.file(output) + mp3_digest = ::Digest::MD5.file(output_mp3) + self["preview_md5"] = ogg_md5 = ogg_digest.hexdigest + self["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest + + @@log.debug("uploading ogg preview to #{self.preview_filename('ogg')}") + s3_public_manager.upload(self.preview_filename(ogg_md5, 'ogg'), output, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest) + @@log.debug("uploading mp3 preview to #{self.preview_filename('mp3')}") + s3_public_manager.upload(self.preview_filename(mp3_md5, 'mp3'), output_mp3, content_type: 'audio/mpeg', content_md5: mp3_digest.base64digest) + + self.skip_uploader = true + + original_ogg_preview_url = self["preview_url"] + original_mp3_preview_url = self["preview_mp3_url"] + + # and finally update the JamTrackTrack with the new info + self["preview_url"] = self.preview_filename(ogg_md5, 'ogg') + self["preview_length"] = File.new(output).size + # and finally update the JamTrackTrack with the new info + self["preview_mp3_url"] = self.preview_filename(mp3_md5, 'mp3') + self["preview_mp3_length"] = File.new(output_mp3).size + self.save! + + # if all that worked, now delete old previews, if present + begin + s3_public_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != self["preview_url"] + s3_public_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"] + rescue + puts "UNABLE TO CLEANUP OLD PREVIEW URL" + end + + end + end + end + rescue Exception => e + @@log.error("error in sox command #{e.to_s}") + @preview_generate_error = e.to_s + end + + end + + + private def normalize_position parent = self.jam_track position = 0 diff --git a/ruby/lib/jam_ruby/models/machine_extra.rb b/ruby/lib/jam_ruby/models/machine_extra.rb new file mode 100644 index 000000000..74b1402d0 --- /dev/null +++ b/ruby/lib/jam_ruby/models/machine_extra.rb @@ -0,0 +1,35 @@ +module JamRuby + class MachineExtra < ActiveRecord::Base + + @@log = Logging.logger[MachineExtra] + + belongs_to :machine_fingerprint, :class_name => "JamRuby::MachineFingerprint" + + def self.create(machine_fingerprint, data) + me = MachineExtra.new + me.machine_fingerprint = machine_fingerprint + me.mac_address = data[:mac] + me.mac_name = data[:name] + me.upstate = data[:upstate] + me.ipaddr_0 = data[:ipaddr_0] + me.ipaddr_1 = data[:ipaddr_1] + me.ipaddr_2 = data[:ipaddr_2] + me.ipaddr_3 = data[:ipaddr_3] + me.ipaddr_4 = data[:ipaddr_4] + me.ipaddr_5 = data[:ipaddr_5] + me.save + + unless me.save + @@log.error("unable to create machine extra: #{me.errors.inspect}") + end + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/machine_extras/" + id + end + + def to_s + "#{mac_address} #{mac_name} #{upstate ? 'UP' : 'DOWN'} #{ipaddr_0} #{ipaddr_1} #{ipaddr_2} #{ipaddr_3} #{ipaddr_4} #{ipaddr_5}" + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/machine_fingerprint.rb b/ruby/lib/jam_ruby/models/machine_fingerprint.rb new file mode 100644 index 000000000..d4d668052 --- /dev/null +++ b/ruby/lib/jam_ruby/models/machine_fingerprint.rb @@ -0,0 +1,49 @@ +module JamRuby + class MachineFingerprint < ActiveRecord::Base + + @@log = Logging.logger[MachineFingerprint] + + belongs_to :user, :class_name => "JamRuby::User" + belongs_to :jam_track_right, :class_name => "JamRuby::JamTrackRight" + has_one :detail, :class_name => "JamRuby::MachineExtra" + belongs_to :fingerprint_whitelist, class_name: 'JamRuby::FingerprintWhitelist', foreign_key: :fingerprint + + TAKEN_ON_SUCCESSFUL_DOWNLOAD = 'dl' + TAKEN_ON_FRAUD_CONFLICT = 'fc' + + PRINT_TYPE_ALL = 'a' + PRINT_TYPE_ACTIVE = 'r' + + + validates :user, presence:true + validates :when_taken, :inclusion => {:in => [TAKEN_ON_SUCCESSFUL_DOWNLOAD, TAKEN_ON_FRAUD_CONFLICT]} + validates :fingerprint, presence: true + validates :print_type, presence: true, :inclusion => {:in =>[PRINT_TYPE_ALL, PRINT_TYPE_ACTIVE]} + validates :remote_ip, presence: true + + def self.create(fingerprint, user, when_taken, print_type, remote_ip, extra, jam_track_right = nil) + mf = MachineFingerprint.new + mf.fingerprint = fingerprint + mf.user = user + mf.when_taken = when_taken + mf.print_type = print_type + mf.remote_ip = remote_ip + mf.jam_track_right = jam_track_right + if mf.save + MachineExtra.create(mf, extra) if extra + else + + @@log.error("unable to create machine fingerprint: #{mf.errors.inspect}") + end + mf + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/machine_fingerprints/" + id + end + + def to_s + "#{fingerprint} #{remote_ip} #{user} #{detail}" + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/max_mind_release.rb b/ruby/lib/jam_ruby/models/max_mind_release.rb index ebc931abc..3639b2e93 100644 --- a/ruby/lib/jam_ruby/models/max_mind_release.rb +++ b/ruby/lib/jam_ruby/models/max_mind_release.rb @@ -131,9 +131,10 @@ module JamRuby end end - uri = URI(sign_url(field)) + url = sign_url(field) + uri = URI(url) open downloaded_filename, 'wb' do |io| - Net::HTTP.start(uri.host, uri.port) do |http| + Net::HTTP.start(uri.host, uri.port, use_ssl: url.start_with?('https') ? true : false) do |http| request = Net::HTTP::Get.new uri http.request request do |response| response_code = response.code.to_i diff --git a/ruby/lib/jam_ruby/models/mix.rb b/ruby/lib/jam_ruby/models/mix.rb index d5c7bd364..b58a4af05 100644 --- a/ruby/lib/jam_ruby/models/mix.rb +++ b/ruby/lib/jam_ruby/models/mix.rb @@ -4,6 +4,8 @@ module JamRuby MAX_MIX_TIME = 7200 # 2 hours + @@log = Logging.logger[Mix] + before_destroy :delete_s3_files self.primary_key = 'id' @@ -137,15 +139,67 @@ module JamRuby one_day = 60 * 60 * 24 jam_track_offset = 0 + jam_track_seek = 0 + + was_jamtrack_played = false if recording.timeline recording_timeline_data = JSON.parse(recording.timeline) + # did the jam track play at all? + jam_track_isplaying = recording_timeline_data["jam_track_isplaying"] recording_start_time = recording_timeline_data["recording_start_time"] jam_track_play_start_time = recording_timeline_data["jam_track_play_start_time"] jam_track_recording_start_play_offset = recording_timeline_data["jam_track_recording_start_play_offset"] - jam_track_offset = -jam_track_recording_start_play_offset + if jam_track_play_start_time != 0 + was_jamtrack_played = true + + # how long did the JamTrack play? not needed because we limit on the input tracks, which represents how long the recording is, too + jam_track_play_time = recording_timeline_data["jam_track_play_time"] + + + offset = jam_track_play_start_time - recording_start_time + + @@log.debug("base offset = #{offset}") + if offset >= 0 + # jamtrack started after recording, so buffer with silence as necessary\ + + if jam_track_recording_start_play_offset < 0 + @@log.info("prelude captured. offsetting further by #{-jam_track_recording_start_play_offset}") + # a negative jam_track_recording_start_play_offset indicates prelude, i.e., silence + # so add it to the offset to add more silence as necessary + offset = offset + -jam_track_recording_start_play_offset + jam_track_offset = offset + else + @@log.info("positive jamtrack offset; seeking into jamtrack by #{jam_track_recording_start_play_offset}") + # a positive jam_track_recording_start_play_offset means we need to cut into the jamtrack + jam_track_seek = jam_track_recording_start_play_offset + jam_track_offset = offset + end + else + # jamtrack started before recording, so we can seek into it to make up for the missing parts + + if jam_track_recording_start_play_offset < 0 + @@log.info("partial prelude captured. offset becomes jamtrack offset#{-jam_track_recording_start_play_offset}") + # a negative jam_track_recording_start_play_offset indicates prelude, i.e., silence + # so add it to the offset to add more silence as necessary + jam_track_offset = -jam_track_recording_start_play_offset + else + @@log.info("no prelude captured. offset becomes jamtrack offset=#{jam_track_recording_start_play_offset}") + + jam_track_offset = 0 + jam_track_seek = jam_track_recording_start_play_offset + end + + + # also, ignore jam_track_recording_start_play_offset - it simply matches the offset in this case + end + + @@log.info("computed values. jam_track_offset=#{jam_track_offset} jam_track_seek=#{jam_track_seek}") + + + end end manifest = { "files" => [], "timeline" => [] } @@ -154,7 +208,7 @@ module JamRuby # this 'pick limiter' logic will ensure that we set a limiter on the 1st recorded_track we come across. pick_limiter = false - if recording.is_jamtrack_recording? + if was_jamtrack_played # we only use the limiter feature if this is a JamTrack recording # by setting this to true, the 1st recorded_track in the database will be the limiter pick_limiter = true @@ -171,27 +225,29 @@ module JamRuby mix_params << { "level" => 1.0, "balance" => 0 } end - recording.recorded_jam_track_tracks.each do |recorded_jam_track_track| - manifest["files"] << { "filename" => recorded_jam_track_track.jam_track_track.sign_url(one_day), "codec" => "vorbis", "offset" => jam_track_offset } - # let's look for level info from the client - level = 1.0 # default value - means no effect - if recorded_jam_track_track.timeline + if was_jamtrack_played + recording.recorded_jam_track_tracks.each do |recorded_jam_track_track| + manifest["files"] << { "filename" => recorded_jam_track_track.jam_track_track.sign_url(one_day, sample_rate=44), "codec" => "vorbis", "offset" => jam_track_offset, "seek" => jam_track_seek } + # let's look for level info from the client + level = 1.0 # default value - means no effect + if recorded_jam_track_track.timeline - timeline_data = JSON.parse(recorded_jam_track_track.timeline) + timeline_data = JSON.parse(recorded_jam_track_track.timeline) - # always take the 1st entry for now - first = timeline_data[0] + # always take the 1st entry for now + first = timeline_data[0] - if first["mute"] - # mute equates to no noise - level = 0.0 - else - # otherwise grab the left channel... - level = first["vol_l"] + if first["mute"] + # mute equates to no noise + level = 0.0 + else + # otherwise grab the left channel... + level = first["vol_l"] + end end - end - mix_params << { "level" => level, "balance" => 0 } + mix_params << { "level" => level, "balance" => 0 } + end end manifest["timeline"] << { "timestamp" => 0, "mix" => mix_params } @@ -200,14 +256,47 @@ module JamRuby manifest end + + def local_manifest + remote_manifest = self.manifest + remote_manifest["files"].each do |file| + filename = file["filename"] + + basename = File.basename(filename) + basename = basename[0..(basename.index('?') - 1)] + + file["filename"] = basename + end + + # update manifest so that audiomixer writes here + remote_manifest["output"]["filename"] = 'out.ogg' + # update manifest so that audiomixer writes here + remote_manifest["error_out"] = 'error.out' + remote_manifest["mix_id"] = self.id + remote_manifest + end + + def download_script + out = '' + + remote_manifest = manifest + remote_manifest["files"].each do |file| + filename = file["filename"] + basename = File.basename(filename) + basename = basename[0..(basename.index('?') - 1)] + + out << "curl -o \"#{basename}\" \"#{filename}\"\r\n\r\n" + end + out << "\r\n\r\n" + out + end + def s3_url(type='ogg') if type == 'ogg' s3_manager.s3_url(self[:ogg_url]) else s3_manager.s3_url(self[:mp3_url]) end - - end def is_completed @@ -216,7 +305,7 @@ module JamRuby # if the url starts with http, just return it because it's in some other store. Otherwise it's a relative path in s3 and needs be signed def resolve_url(url_field, mime_type, expiration_time) - self[url_field].start_with?('http') ? self[url_field] : s3_manager.sign_url(self[url_field], {:expires => expiration_time, :response_content_type => mime_type, :secure => false}) + self[url_field].start_with?('http') ? self[url_field] : s3_manager.sign_url(self[url_field], {:expires => expiration_time, :response_content_type => mime_type, :secure => true}) end def sign_url(expiration_time = 120, type='ogg') diff --git a/ruby/lib/jam_ruby/models/music_notation.rb b/ruby/lib/jam_ruby/models/music_notation.rb index d7fec23db..fc2af0880 100644 --- a/ruby/lib/jam_ruby/models/music_notation.rb +++ b/ruby/lib/jam_ruby/models/music_notation.rb @@ -39,7 +39,7 @@ module JamRuby end def sign_url(expiration_time = 120) - s3_manager.sign_url(self[:file_url], {:expires => expiration_time, :secure => false}) + s3_manager.sign_url(self[:file_url], {:expires => expiration_time, :secure => true}) end private diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index e2954d9f3..96b9b7831 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -282,7 +282,7 @@ module JamRuby return query end - def self.scheduled user + def self.scheduled user, only_public = false # keep unstarted sessions around for 12 hours after scheduled_start session_not_started = "(music_sessions.scheduled_start > NOW() - '12 hour'::INTERVAL AND music_sessions.started_at IS NULL)" @@ -293,6 +293,7 @@ module JamRuby session_finished = "(music_sessions.session_removed_at > NOW() - '2 hour'::INTERVAL)" query = MusicSession.where("music_sessions.canceled = FALSE") + query = query.where('music_sessions.fan_access = TRUE or music_sessions.musician_access = TRUE') if only_public query = query.where("music_sessions.user_id = '#{user.id}'") query = query.where("music_sessions.scheduled_start IS NULL OR #{session_not_started} OR #{session_finished} OR #{session_started_not_finished}") query = query.where("music_sessions.create_type IS NULL OR music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}'") diff --git a/ruby/lib/jam_ruby/models/music_session_user_history.rb b/ruby/lib/jam_ruby/models/music_session_user_history.rb index 430c897a1..bca0ecccc 100644 --- a/ruby/lib/jam_ruby/models/music_session_user_history.rb +++ b/ruby/lib/jam_ruby/models/music_session_user_history.rb @@ -43,7 +43,7 @@ module JamRuby session_user_history.music_session_id = music_session_id session_user_history.user_id = user_id session_user_history.client_id = client_id - session_user_history.instruments = tracks.map {|t| t[:instrument_id]}.join("|") + session_user_history.instruments = tracks.map {|t| t[:instrument_id]}.join("|") if tracks session_user_history.save end diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index e0ec6590b..33ac67145 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -210,7 +210,7 @@ module JamRuby return "New message about session." when NotificationTypes::JAM_TRACK_SIGN_COMPLETE - return "Jam Track is ready for download." + return "JamTrack is ready for download." # recording notifications when NotificationTypes::MUSICIAN_RECORDING_SAVED diff --git a/ruby/lib/jam_ruby/models/payment_history.rb b/ruby/lib/jam_ruby/models/payment_history.rb new file mode 100644 index 000000000..4862e4dd1 --- /dev/null +++ b/ruby/lib/jam_ruby/models/payment_history.rb @@ -0,0 +1,39 @@ +module JamRuby + class PaymentHistory < ActiveRecord::Base + + self.table_name = 'payment_histories' + + belongs_to :sale + belongs_to :recurly_transaction_web_hook + + + def self.index(user, params = {}) + + limit = params[:per_page] + limit ||= 20 + limit = limit.to_i + + query = PaymentHistory.limit(limit) + .includes(sale: [:sale_line_items], recurly_transaction_web_hook:[]) + .where(user_id: user.id) + .where("transaction_type = 'sale' OR transaction_type = 'refund' OR transaction_type = 'void'") + .order('created_at DESC') + + + current_page = params[:page].nil? ? 1 : params[:page].to_i + next_page = current_page + 1 + + # will_paginate gem + query = query.paginate(:page => current_page, :per_page => limit) + + if query.length == 0 # no more results + { query: query, next_page: nil} + elsif query.length < limit # no more results + { query: query, next_page: nil} + else + { query: query, next_page: next_page } + end + + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/quick_mix.rb b/ruby/lib/jam_ruby/models/quick_mix.rb index 18da130c8..4053fdaa3 100644 --- a/ruby/lib/jam_ruby/models/quick_mix.rb +++ b/ruby/lib/jam_ruby/models/quick_mix.rb @@ -219,7 +219,7 @@ module JamRuby # if the url starts with http, just return it because it's in some other store. Otherwise it's a relative path in s3 and needs be signed def resolve_url(url_field, mime_type, expiration_time) - self[url_field].start_with?('http') ? self[url_field] : s3_manager.sign_url(self[url_field], {:expires => expiration_time, :response_content_type => mime_type, :secure => false}) + self[url_field].start_with?('http') ? self[url_field] : s3_manager.sign_url(self[url_field], {:expires => expiration_time, :response_content_type => mime_type, :secure => true}) end def sign_url(expiration_time = 120, type='ogg') @@ -232,6 +232,7 @@ module JamRuby end end + # this is not 'secure' because, in testing, the PUT failed often in Ruby. should investigate more. def sign_put(expiration_time = 3600 * 24, type='ogg') type ||= 'ogg' if type == 'ogg' diff --git a/ruby/lib/jam_ruby/models/recorded_backing_track.rb b/ruby/lib/jam_ruby/models/recorded_backing_track.rb index 0826d160c..533f6aa5d 100644 --- a/ruby/lib/jam_ruby/models/recorded_backing_track.rb +++ b/ruby/lib/jam_ruby/models/recorded_backing_track.rb @@ -41,7 +41,7 @@ module JamRuby end def sign_url(expiration_time = 120) - s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false}) + s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => true}) end def can_download?(some_user) diff --git a/ruby/lib/jam_ruby/models/recorded_track.rb b/ruby/lib/jam_ruby/models/recorded_track.rb index e11a76bf0..62fdfa1e5 100644 --- a/ruby/lib/jam_ruby/models/recorded_track.rb +++ b/ruby/lib/jam_ruby/models/recorded_track.rb @@ -148,7 +148,7 @@ module JamRuby end def sign_url(expiration_time = 120) - s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false}) + s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => true}) end def upload_start(length, md5) diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb index cdb72816a..99f21c09e 100644 --- a/ruby/lib/jam_ruby/models/recording.rb +++ b/ruby/lib/jam_ruby/models/recording.rb @@ -3,7 +3,7 @@ module JamRuby @@log = Logging.logger[Recording] - attr_accessible :owner, :owner_id, :band, :band_id, :recorded_tracks_attributes, :mixes_attributes, :claimed_recordings_attributes, :name, :description, :genre, :is_public, :duration, as: :admin + attr_accessible :owner, :owner_id, :band, :band_id, :recorded_tracks_attributes, :mixes_attributes, :claimed_recordings_attributes, :name, :description, :genre, :is_public, :duration, :jam_track_id, as: :admin has_many :users, :through => :recorded_tracks, :class_name => "JamRuby::User" has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :recording, :foreign_key => 'recording_id', :dependent => :destroy @@ -21,6 +21,7 @@ module JamRuby belongs_to :owner, :class_name => "JamRuby::User", :inverse_of => :owned_recordings, :foreign_key => 'owner_id' belongs_to :band, :class_name => "JamRuby::Band", :inverse_of => :recordings belongs_to :music_session, :class_name => "JamRuby::ActiveMusicSession", :inverse_of => :recordings, foreign_key: :music_session_id + belongs_to :non_active_music_session, :class_name => "JamRuby::MusicSession", foreign_key: :music_session_id belongs_to :jam_track, :class_name => "JamRuby::JamTrack", :inverse_of => :recordings, :foreign_key => 'jam_track_id' belongs_to :jam_track_initiator, :class_name => "JamRuby::User", :inverse_of => :initiated_jam_track_recordings, :foreign_key => 'jam_track_initiator_id' @@ -50,7 +51,11 @@ module JamRuby end def is_jamtrack_recording? - !jam_track_id.nil? + !jam_track_id.nil? && parsed_timeline['jam_track_isplaying'] + end + + def parsed_timeline + timeline ? JSON.parse(timeline) : {} end def high_quality_mix? @@ -182,21 +187,21 @@ module JamRuby def recorded_tracks_for_user(user) unless self.users.exists?(user) - raise PermissionError, "user was not in this session" + raise JamPermissionError, "user was not in this session" end recorded_tracks.where(:user_id => user.id) end def recorded_backing_tracks_for_user(user) unless self.users.exists?(user) - raise PermissionError, "user was not in this session" + raise JamPermissionError, "user was not in this session" end recorded_backing_tracks.where(:user_id => user.id) end def has_access?(user) - users.exists?(user) + users.exists?(user) || plays.where("player_id=?", user).count != 0 end # Start recording a session. @@ -264,7 +269,7 @@ module JamRuby def claim(user, name, description, genre, is_public, upload_to_youtube=false) upload_to_youtube = !!upload_to_youtube # Correct where nil is borking save unless self.users.exists?(user) - raise PermissionError, "user was not in this session" + raise JamPermissionError, "user was not in this session" end claimed_recording = ClaimedRecording.new @@ -710,23 +715,26 @@ module JamRuby end end + def self.popular_recordings(limit = 100) + Recording.select('recordings.id').joins('inner join claimed_recordings ON claimed_recordings.recording_id = recordings.id AND claimed_recordings.is_public = TRUE').where(all_discarded: false).where(is_done: true).where(deleted: false).order('play_count DESC').limit(limit).group('recordings.id') + end private def self.validate_user_is_band_member(user, band) unless band.users.exists? user - raise PermissionError, ValidationMessages::USER_NOT_BAND_MEMBER_VALIDATION_ERROR + raise JamPermissionError, ValidationMessages::USER_NOT_BAND_MEMBER_VALIDATION_ERROR end end def self.validate_user_is_creator(user, creator) unless user.id == creator.id - raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR + raise JamPermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR end end def self.validate_user_is_musician(user) unless user.musician? - raise PermissionError, ValidationMessages::USER_NOT_MUSICIAN_VALIDATION_ERROR + raise JamPermissionError, ValidationMessages::USER_NOT_MUSICIAN_VALIDATION_ERROR end end diff --git a/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb b/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb index 0139cba9e..9d0fc9bd4 100644 --- a/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb +++ b/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb @@ -1,20 +1,42 @@ module JamRuby - class RecurlyTransactionWebHook < ActiveRecord::Base + class RecurlyTransactionWebHook < ActiveRecord::Base + + attr_accessible :admin_description, :jam_track_id, as: :admin belongs_to :user, class_name: 'JamRuby::User' + belongs_to :sale_line_item, class_name: 'JamRuby::SaleLineItem', foreign_key: 'subscription_id', primary_key: 'recurly_subscription_uuid', inverse_of: :recurly_transactions + belongs_to :sale, class_name: 'JamRuby::Sale', foreign_key: 'invoice_id', primary_key: 'recurly_invoice_id', inverse_of: :recurly_transactions + + # when we know what JamTrack this refund is related to, we set this value + belongs_to :jam_track, class_name: 'JamRuby::JamTrack' validates :recurly_transaction_id, presence: true - validates :subscription_id, presence: true validates :action, presence: true validates :status, presence: true validates :amount_in_cents, numericality: {only_integer: true} validates :user, presence: true + SUCCESSFUL_PAYMENT = 'payment' FAILED_PAYMENT = 'failed_payment' REFUND = 'refund' VOID = 'void' + + HOOK_TYPES = [SUCCESSFUL_PAYMENT, FAILED_PAYMENT, REFUND, VOID] + + def is_credit_type? + transaction_type == REFUND || transaction_type == VOID + end + + def is_voided? + transaction_type == VOID + end + + def is_refund? + transaction_type == REFUND + end + def self.is_transaction_web_hook?(document) return false if document.root.nil? @@ -32,6 +54,10 @@ module JamRuby end end + def admin_url + APP_CONFIG.admin_root_url + "/admin/recurly_hooks/" + id + end + # see spec for examples of XML def self.create_from_xml(document) @@ -67,9 +93,60 @@ module JamRuby # now that we have the transaction saved, we also need to delete the jam_track_right if this is a refund, or voided + if transaction.transaction_type == 'refund' || transaction.transaction_type == 'void' - right = JamTrackRight.find_by_recurly_subscription_uuid(transaction.subscription_id) - right.destroy if right + sale = Sale.find_by_recurly_invoice_id(transaction.invoice_id) + + if sale && sale.is_jam_track_sale? + if sale.sale_line_items.length == 1 + if sale.recurly_total_in_cents == transaction.amount_in_cents + line_item = sale.sale_line_items[0] + jam_track = line_item.product + jam_track_right = jam_track.right_for_user(transaction.user) if jam_track + if jam_track_right + line_item.affiliate_refunded = true + line_item.affiliate_refunded_at = Time.now + line_item.save! + + jam_track_right.destroy + + # associate which JamTrack we assume this is related to in this one success case + transaction.jam_track = jam_track + transaction.save! + + AdminMailer.recurly_alerts(transaction.user, { + subject: "NOTICE: #{transaction.user.email} has had JamTrack: #{jam_track.name} revoked", + body: "A #{transaction.transaction_type} event came from Recurly for sale with Recurly invoice ID #{sale.recurly_invoice_id}. We deleted their right to the track in our own database as a result." + }).deliver + else + AdminMailer.recurly_alerts(transaction.user, { + subject: "NOTICE: #{transaction.user.email} got a refund, but unable to find JamTrackRight to delete", + body: "This should just mean the user already has no rights to the JamTrackRight when the refund came in. Not a big deal, but sort of weird..." + }).deliver + end + + else + AdminMailer.recurly_alerts(transaction.user, { + subject: "ACTION REQUIRED: #{transaction.user.email} got a refund it was not for total value of a JamTrack sale", + body: "We received a #{transaction.transaction_type} notice for an amount that was not the same as the original sale. So, no action was taken in the database. sale total: #{sale.recurly_total_in_cents}, refund amount: #{transaction.amount_in_cents}" + }).deliver + end + + + else + AdminMailer.recurly_alerts(transaction.user, { + subject: "ACTION REQUIRED: #{transaction.user.email} has refund on invoice with multiple JamTracks", + body: "You will have to manually revoke any JamTrackRights in our database for the appropriate JamTracks" + }).deliver + end + else + AdminMailer.recurly_alerts(transaction.user, { + subject: "ACTION REQUIRED: #{transaction.user.email} has refund with no correlator to sales", + body: "You will have to manually revoke any JamTrackRights in our database for the appropriate JamTracks" + }).deliver + end + + end transaction end diff --git a/ruby/lib/jam_ruby/models/rsvp_request.rb b/ruby/lib/jam_ruby/models/rsvp_request.rb index 9f233b973..f416640bf 100644 --- a/ruby/lib/jam_ruby/models/rsvp_request.rb +++ b/ruby/lib/jam_ruby/models/rsvp_request.rb @@ -62,7 +62,7 @@ module JamRuby invitation = Invitation.where("music_session_id = ? AND receiver_id = ?", music_session.id, user.id) if invitation.first.nil? && !music_session.open_rsvps && music_session.creator.id != user.id - raise PermissionError, "Only a session invitee can create an RSVP for this session." + raise JamPermissionError, "Only a session invitee can create an RSVP for this session." end RsvpRequest.transaction do @@ -154,7 +154,7 @@ module JamRuby # authorize the user attempting to respond to the RSVP request if music_session.creator.id != user.id - raise PermissionError, "Only the session organizer can accept or decline an RSVP request." + raise JamPermissionError, "Only the session organizer can accept or decline an RSVP request." end rsvp_request = RsvpRequest.find_by_id(rsvp_request_id) @@ -249,7 +249,7 @@ module JamRuby rsvp_request = RsvpRequest.find(params[:id]) if music_session.creator.id != user.id && rsvp_request.user_id != user.id - raise PermissionError, "Only the session organizer or RSVP creator can cancel the RSVP." + raise JamPermissionError, "Only the session organizer or RSVP creator can cancel the RSVP." end RsvpRequest.transaction do diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb index 63f42657b..bca02f526 100644 --- a/ruby/lib/jam_ruby/models/sale.rb +++ b/ruby/lib/jam_ruby/models/sale.rb @@ -3,30 +3,370 @@ module JamRuby # a sale is created every time someone tries to buy something class Sale < ActiveRecord::Base + JAMTRACK_SALE = 'jamtrack' + belongs_to :user, class_name: 'JamRuby::User' has_many :sale_line_items, class_name: 'JamRuby::SaleLineItem' - validates :order_total, numericality: { only_integer: false } + has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale, foreign_key: 'invoice_id', primary_key: 'recurly_invoice_id' + + validates :order_total, numericality: {only_integer: false} validates :user, presence: true - def self.create(user) + @@log = Logging.logger[Sale] + + def self.index(user, params = {}) + + limit = params[:per_page] + limit ||= 20 + limit = limit.to_i + + query = Sale.limit(limit) + .includes([:recurly_transactions, :sale_line_items]) + .where('sales.user_id' => user.id) + .order('sales.created_at DESC') + + current_page = params[:page].nil? ? 1 : params[:page].to_i + next_page = current_page + 1 + + # will_paginate gem + query = query.paginate(:page => current_page, :per_page => limit) + + if query.length == 0 # no more results + { query: query, next_page: nil} + elsif query.length < limit # no more results + { query: query, next_page: nil} + else + { query: query, next_page: next_page } + end + + end + + def state + original_total = self.recurly_total_in_cents + + is_voided = false + refund_total = 0 + + recurly_transactions.each do |transaction| + if transaction.is_voided? + is_voided = true + else + + end + + if transaction.is_refund? + refund_total = refund_total + transaction.amount_in_cents + end + end + + # if refund_total is > 0, then you have a refund. + # if voided is true, then in theory the whole thing has been refunded + { + voided: is_voided, + original_total: original_total, + refund_total: refund_total + } + end + + def self.preview_invoice(current_user, shopping_carts) + + line_items = {jam_tracks: []} + shopping_carts_jam_tracks = [] + shopping_carts_subscriptions = [] + shopping_carts.each do |shopping_cart| + + if shopping_cart.is_jam_track? + shopping_carts_jam_tracks << shopping_cart + else + # XXX: this may have to be revisited when we actually have something other than JamTracks for puchase + shopping_carts_subscriptions << shopping_cart + end + end + + jam_track_items = preview_invoice_jam_tracks(current_user, shopping_carts_jam_tracks) + line_items[:jam_tracks] = jam_track_items if jam_track_items + + # TODO: process shopping_carts_subscriptions + + line_items + end + + # place_order will create one or more sales based on the contents of shopping_carts for the current user + # individual subscriptions will end up create their own sale (you can't have N subscriptions in one sale--recurly limitation) + # jamtracks however can be piled onto the same sale as adjustments (VRFS-3028) + # so this method may create 1 or more sales, , where 2 or more sales can occur if there are more than one subscriptions or subscription + jamtrack + def self.place_order(current_user, shopping_carts) + + sales = [] + shopping_carts_jam_tracks = [] + shopping_carts_subscriptions = [] + shopping_carts.each do |shopping_cart| + + if shopping_cart.is_jam_track? + shopping_carts_jam_tracks << shopping_cart + else + # XXX: this may have to be revisited when we actually have something other than JamTracks for puchase + shopping_carts_subscriptions << shopping_cart + end + end + + jam_track_sale = order_jam_tracks(current_user, shopping_carts_jam_tracks) + sales << jam_track_sale if jam_track_sale + + # TODO: process shopping_carts_subscriptions + + sales + end + + def self.preview_invoice_jam_tracks(current_user, shopping_carts_jam_tracks) + ### XXX TODO; + + # we currently use a fake plan in Recurly to estimate taxes using the Pricing.Attach metod in Recurly.js + + # if we were to implement this the right way (ensure adjustments are on the account as necessary), then it would be better (more correct) + # just a pain to implement + end + + def self.is_only_freebie(shopping_carts_jam_tracks) + shopping_carts_jam_tracks.length == 1 && shopping_carts_jam_tracks[0].product_info[:free] + end + + # this method will either return a valid sale, or throw a RecurlyClientError or ActiveRecord validation error (save! failed) + # it may return an nil sale if the JamTrack(s) specified by the shopping carts are already owned + def self.order_jam_tracks(current_user, shopping_carts_jam_tracks) + + client = RecurlyClient.new + + sale = nil + Sale.transaction do + sale = create_jam_track_sale(current_user) + + if sale.valid? + if is_only_freebie(shopping_carts_jam_tracks) + sale.process_jam_tracks(current_user, shopping_carts_jam_tracks, nil) + + sale.recurly_subtotal_in_cents = 0 + sale.recurly_tax_in_cents = 0 + sale.recurly_total_in_cents = 0 + sale.recurly_currency = 'USD' + + sale_line_item = sale.sale_line_items[0] + sale_line_item.recurly_tax_in_cents = 0 + sale_line_item.recurly_total_in_cents = 0 + sale_line_item.recurly_currency = 'USD' + sale_line_item.recurly_discount_in_cents = 0 + sale.save + + else + + account = client.get_account(current_user) + if account.present? + + purge_pending_adjustments(account) + + created_adjustments = sale.process_jam_tracks(current_user, shopping_carts_jam_tracks, account) + + # now invoice the sale ... almost done + + begin + invoice = account.invoice! + sale.recurly_invoice_id = invoice.uuid + sale.recurly_invoice_number = invoice.invoice_number + + # now slap in all the real tax/purchase totals + sale.recurly_subtotal_in_cents = invoice.subtotal_in_cents + sale.recurly_tax_in_cents = invoice.tax_in_cents + sale.recurly_total_in_cents = invoice.total_in_cents + sale.recurly_currency = invoice.currency + + # and resolve against sale_line_items + sale.sale_line_items.each do |sale_line_item| + found_line_item = false + invoice.line_items.each do |line_item| + if line_item.uuid == sale_line_item.recurly_adjustment_uuid + sale_line_item.recurly_tax_in_cents = line_item.tax_in_cents + sale_line_item.recurly_total_in_cents =line_item.total_in_cents + sale_line_item.recurly_currency = line_item.currency + sale_line_item.recurly_discount_in_cents = line_item.discount_in_cents + found_line_item = true + break + end + + end + + if !found_line_item + @@log.error("can't find line item #{sale_line_item.recurly_adjustment_uuid}") + puts "CANT FIND LINE ITEM" + end + end + + unless sale.save + raise RecurlyClientError, "Invalid sale (at end)." + end + rescue Recurly::Resource::Invalid => e + # this exception is thrown by invoice! if the invoice is invalid + sale.rollback_adjustments(current_user, created_adjustments) + sale = nil + raise ActiveRecord::Rollback # kill all db activity, but don't break outside logic + end + else + raise RecurlyClientError, "Could not find account to place order." + end + end + else + raise RecurlyClientError, "Invalid sale." + end + end + sale + end + + def process_jam_tracks(current_user, shopping_carts_jam_tracks, account) + + created_adjustments = [] + + begin + shopping_carts_jam_tracks.each do |shopping_cart| + process_jam_track(current_user, shopping_cart, account, created_adjustments) + end + rescue Recurly::Error, NoMethodError => x + # rollback any adjustments created if error + rollback_adjustments(user, created_adjustments) + raise RecurlyClientError, x.to_s + rescue Exception => e + # rollback any adjustments created if error + rollback_adjustments(user, created_adjustments) + raise e + end + + created_adjustments + end + + + def process_jam_track(current_user, shopping_cart, account, created_adjustments) + recurly_adjustment_uuid = nil + recurly_adjustment_credit_uuid = nil + + # we do this because of ShoppingCart.remove_jam_track_from_cart; if it occurs, which should be rare, we need fresh shopping cart info + shopping_cart.reload + + # get the JamTrack in this shopping cart + jam_track = shopping_cart.cart_product + + if jam_track.right_for_user(current_user) + # if the user already owns the JamTrack, we should just skip this cart item, and destroy it + # if this occurs, we have to reload every shopping_cart as we iterate. so, we do at the top of the loop + ShoppingCart.remove_jam_track_from_cart(current_user, shopping_cart) + return + end + + if account + # ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack + adjustments = shopping_cart.create_adjustment_attributes(current_user) + + adjustments.each do |adjustment| + + # create the adjustment at Recurly (this may not look like it, but it is a REST API) + created_adjustment = account.adjustments.new(adjustment) + created_adjustment.save + + # if the adjustment could not be made, bail + raise RecurlyClientError.new(created_adjustment.errors) if created_adjustment.errors.any? + + # keep track of adjustments we created for this order, in case we have to roll them back + created_adjustments << created_adjustment + + if ShoppingCart.is_product_purchase?(adjustment) + # this was a normal product adjustment, so track it as such + recurly_adjustment_uuid = created_adjustment.uuid + else + # this was a 'credit' adjustment, so track it as such + recurly_adjustment_credit_uuid = created_adjustment.uuid + end + end + end + + + # create one sale line item for every jam track + sale_line_item = SaleLineItem.create_from_shopping_cart(self, shopping_cart, nil, recurly_adjustment_uuid, recurly_adjustment_credit_uuid) + + # if the sale line item is invalid, blow up the transaction + unless sale_line_item.valid? + @log.error("sale item invalid! #{sale_line_item.errors.inspect}") + puts("sale item invalid! #{sale_line_item.errors.inspect}") + Stats.write('web.recurly.purchase.sale_invalid', {message: sale_line_item.errors.to_s, value: 1}) + raise RecurlyClientError.new(sale_line_item.errors) + end + + # create a JamTrackRight (this needs to be in a transaction too to make sure we don't make these by accident) + jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) do |jam_track_right| + jam_track_right.redeemed = shopping_cart.free? + end + + # also if the purchase was a free one, then update the user record to no longer allow redeemed jamtracks + if shopping_cart.free? + User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false) + current_user.has_redeemable_jamtrack = false # make sure model reflects the truth + end + + + # this can't go in the block above, as it's here to fix bad subscription UUIDs in an update path + if jam_track_right.recurly_adjustment_uuid != recurly_adjustment_uuid + jam_track_right.recurly_adjustment_uuid = recurly_adjustment_uuid + jam_track_right.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid + unless jam_track_right.save + raise RecurlyClientError.new(jam_track_right.errors) + end + end + + # delete the shopping cart; it's been dealt with + shopping_cart.destroy if shopping_cart + + # blow up the transaction if the JamTrackRight did not get created + raise RecurlyClientError.new(jam_track_right.errors) if jam_track_right.errors.any? + end + + + def rollback_adjustments(current_user, adjustments) + begin + adjustments.each { |adjustment| adjustment.destroy } + rescue Exception => e + AdminMailer.alerts({ + subject: "ACTION REQUIRED: #{current_user.email} did not have all of his adjustments destroyed in rollback", + body: "go delete any adjustments on the account that don't belong. error: #{e}\n\nAdjustments: #{adjustments.inspect}" + }).deliver + + end + end + + def self.purge_pending_adjustments(account) + account.adjustments.pending.find_each do |adjustment| + # we only pre-emptively destroy pending adjustments if they appear to be created by the server + adjustment.destroy if ShoppingCart.is_server_pending_adjustment?(adjustment) + end + end + + def is_jam_track_sale? + sale_type == JAMTRACK_SALE + end + + def self.create_jam_track_sale(user) sale = Sale.new sale.user = user + sale.sale_type = JAMTRACK_SALE sale.order_total = 0 sale.save sale end - def self.check_integrity - SaleLineItem.select([:total, :not_known, :succeeded, :failed, :refunded, :voided]).find_by_sql( - "SELECT COUNT(sale_line_items.id) AS total, - COUNT(CASE WHEN transactions.id IS NULL THEN 1 ELSE null END) not_known, - COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::SUCCESSFUL_PAYMENT}' THEN 1 ELSE null END) succeeded, - COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::FAILED_PAYMENT}' THEN 1 ELSE null END) failed, - COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::REFUND}' THEN 1 ELSE null END) refunded, + # this checks just jamtrack sales appropriately + def self.check_integrity_of_jam_track_sales + Sale.select([:total, :voided]).find_by_sql( + "SELECT COUNT(sales.id) AS total, COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::VOID}' THEN 1 ELSE null END) voided - FROM sale_line_items - LEFT OUTER JOIN recurly_transaction_web_hooks as transactions ON subscription_id = recurly_subscription_uuid") + FROM sales + LEFT OUTER JOIN recurly_transaction_web_hooks as transactions ON invoice_id = sales.recurly_invoice_id + WHERE sale_type = '#{JAMTRACK_SALE}'") end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/sale_line_item.rb b/ruby/lib/jam_ruby/models/sale_line_item.rb index f90b460fa..7d744cfe5 100644 --- a/ruby/lib/jam_ruby/models/sale_line_item.rb +++ b/ruby/lib/jam_ruby/models/sale_line_item.rb @@ -1,27 +1,70 @@ module JamRuby class SaleLineItem < ActiveRecord::Base - belongs_to :sale, class_name: 'JamRuby::Sale' - belongs_to :jam_track, class_name: 'JamRuby::JamTrack' - belongs_to :jam_track_right, class_name: 'JamRuby::JamTrackRight' - JAMBLASTER = 'JamBlaster' JAMCLOUD = 'JamCloud' JAMTRACK = 'JamTrack' + belongs_to :sale, class_name: 'JamRuby::Sale' + belongs_to :jam_track, class_name: 'JamRuby::JamTrack' + belongs_to :jam_track_right, class_name: 'JamRuby::JamTrackRight' + belongs_to :affiliate_referral, class_name: 'JamRuby::AffiliatePartner', foreign_key: :affiliate_referral_id + has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale_line_item, foreign_key: 'subscription_id', primary_key: 'recurly_subscription_uuid' + validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK]} validates :unit_price, numericality: {only_integer: false} validates :quantity, numericality: {only_integer: true} validates :free, numericality: {only_integer: true} validates :sales_tax, numericality: {only_integer: false}, allow_nil: true validates :shipping_handling, numericality: {only_integer: false} + validates :affiliate_referral_fee_in_cents, numericality: {only_integer: false}, allow_nil: true validates :recurly_plan_code, presence:true validates :sale, presence:true - def self.create_from_shopping_cart(sale, shopping_cart, recurly_subscription_uuid) + def product + if product_type == JAMTRACK + JamTrack.find_by_id(product_id) + else + raise 'unsupported product type' + end + end + + def product_info + item = product + { name: product.name } if item + end + + def state + voided = false + refunded = false + failed = false + succeeded = false + + recurly_transactions.each do |transaction| + if transaction.transaction_type == RecurlyTransactionWebHook::VOID + voided = true + elsif transaction.transaction_type == RecurlyTransactionWebHook::REFUND + refunded = true + elsif transaction.transaction_type == RecurlyTransactionWebHook::FAILED_PAYMENT + failed = true + elsif transaction.transaction_type == RecurlyTransactionWebHook::SUCCESSFUL_PAYMENT + succeeded = true + end + end + + { + void: voided, + refund: refunded, + fail: failed, + success: succeeded + } + end + + + def self.create_from_shopping_cart(sale, shopping_cart, recurly_subscription_uuid, recurly_adjustment_uuid, recurly_adjustment_credit_uuid) product_info = shopping_cart.product_info - sale.order_total = sale.order_total + product_info[:total_price] + sale.order_total = sale.order_total + product_info[:real_price] sale_line_item = SaleLineItem.new sale_line_item.product_type = shopping_cart.cart_type @@ -33,7 +76,19 @@ module JamRuby sale_line_item.recurly_plan_code = product_info[:plan_code] sale_line_item.product_id = shopping_cart.cart_id sale_line_item.recurly_subscription_uuid = recurly_subscription_uuid - sale_line_item.sale = sale + sale_line_item.recurly_adjustment_uuid = recurly_adjustment_uuid + sale_line_item.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid + + # determine if we need to associate this sale with a partner + user = shopping_cart.user + referral_info = user.should_attribute_sale?(shopping_cart) + + if referral_info + sale_line_item.affiliate_referral = user.affiliate_referral + sale_line_item.affiliate_referral_fee_in_cents = referral_info[:fee_in_cents] + end + + sale.sale_line_items << sale_line_item sale_line_item.save sale_line_item end diff --git a/ruby/lib/jam_ruby/models/shopping_cart.rb b/ruby/lib/jam_ruby/models/shopping_cart.rb index b2bbc13ce..568d94048 100644 --- a/ruby/lib/jam_ruby/models/shopping_cart.rb +++ b/ruby/lib/jam_ruby/models/shopping_cart.rb @@ -1,8 +1,19 @@ module JamRuby class ShoppingCart < ActiveRecord::Base + # just a normal purchase; used on the description field of a recurly adjustment + PURCHASE_NORMAL = 'purchase-normal' + # a free purchase; used on the description field of a recurly adjustment + PURCHASE_FREE = 'purchase-free' + # a techinicality of Recurly; we create a free-credit adjustment to balance out the free purchase adjustment + PURCHASE_FREE_CREDIT = 'purchase-free-credit' + + PURCHASE_REASONS = [PURCHASE_NORMAL, PURCHASE_FREE, PURCHASE_FREE_CREDIT] + attr_accessible :quantity, :cart_type, :product_info + validates_uniqueness_of :cart_id, scope: [:cart_type, :user_id, :anonymous_user_id] + belongs_to :user, :inverse_of => :shopping_carts, :class_name => "JamRuby::User", :foreign_key => "user_id" validates :cart_id, presence: true @@ -14,14 +25,20 @@ module JamRuby def product_info product = self.cart_product - {name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem} unless product.nil? + {name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, real_price: real_price(product), total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem, free: free?, sales_region: product.sales_region} unless product.nil? + end + + # multiply quantity by price + def total_price(product) + quantity * product.price end # multiply (quantity - redeemable) by price - def total_price(product) + def real_price(product) (quantity - marked_for_redeem) * product.price end + def cart_product self.cart_class_name.classify.constantize.find_by_id(self.cart_id) unless self.cart_class_name.blank? end @@ -51,6 +68,68 @@ module JamRuby cart end + def is_jam_track? + cart_type == JamTrack::PRODUCT_TYPE + end + + + # returns an array of adjustments for the shopping cart + def create_adjustment_attributes(current_user) + raise "not a jam track" unless is_jam_track? + + info = self.product_info + + if free? + + # create the credit, then the pseudo charge + [ + { + accounting_code: PURCHASE_FREE_CREDIT, + currency: 'USD', + unit_amount_in_cents: -(info[:total_price] * 100).to_i, + description: "JamTrack: " + info[:name] + " (Credit)", + tax_exempt: true + }, + { + accounting_code: PURCHASE_FREE, + currency: 'USD', + unit_amount_in_cents: (info[:total_price] * 100).to_i, + description: "JamTrack: " + info[:name], + tax_exempt: true + } + ] + else + [ + { + accounting_code: PURCHASE_NORMAL, + currency: 'USD', + unit_amount_in_cents: (info[:total_price] * 100).to_i, + description: "JamTrack: " + info[:name], + tax_exempt: false + } + ] + end + end + + def self.move_to_user(user, anonymous_user, shopping_carts) + shopping_carts.each do |shopping_cart| + mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(user) + cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, mark_redeem) + end + + anonymous_user.destroy_all_shopping_carts + end + + def self.is_product_purchase?(adjustment) + (adjustment[:accounting_code].include?(PURCHASE_FREE) || adjustment[:accounting_code].include?(PURCHASE_NORMAL)) && !adjustment[:accounting_code].include?(PURCHASE_FREE_CREDIT) + end + + # recurly_adjustment is a Recurly::Adjustment (http://www.rubydoc.info/gems/recurly/Recurly/Adjustment) + # this asks, 'is this a pending adjustment?' AND 'was this adjustment created by the server (vs manually by someone -- we should leave those alone).' + def self.is_server_pending_adjustment?(recurly_adjustment) + recurly_adjustment.state == 'pending' && (recurly_adjustment.accounting_code.include?(PURCHASE_FREE) || recurly_adjustment.accounting_code.include?(PURCHASE_NORMAL) || recurly_adjustment.accounting_code.include?(PURCHASE_FREE_CREDIT)) + end + # if the user has a redeemable jam_track still on their account, then also check if any shopping carts have already been marked. # if no shpping carts have been marked, then mark it redeemable # should be wrapped in a TRANSACTION @@ -73,20 +152,14 @@ module JamRuby def self.add_jam_track_to_cart(any_user, jam_track) cart = nil ShoppingCart.transaction do - # does this user already have this JamTrack in their cart? If so, don't add it. - duplicate_found = false - any_user.shopping_carts.each do |shopping_cart| - if shopping_cart.cart_type == JamTrack::PRODUCT_TYPE && shopping_cart.cart_id == jam_track.id - duplicate_found = true - return - end + if any_user.has_redeemable_jamtrack + # if you still have a freebie available to you, or if you are an anonymous user, we make sure there is nothing else in your shopping cart + any_user.destroy_all_shopping_carts end - unless duplicate_found - mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user) - cart = ShoppingCart.create(any_user, jam_track, 1, mark_redeem) - end + mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user) + cart = ShoppingCart.create(any_user, jam_track, 1, mark_redeem) end cart end @@ -106,7 +179,13 @@ module JamRuby carts[0].save end end + end + def port(user, anonymous_user) + + ShoppingCart.transaction do + move_to_user(user, anonymous_user, anonymous_user.shopping_carts) + end end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/signup_hint.rb b/ruby/lib/jam_ruby/models/signup_hint.rb new file mode 100644 index 000000000..c95353d6c --- /dev/null +++ b/ruby/lib/jam_ruby/models/signup_hint.rb @@ -0,0 +1,38 @@ +module JamRuby + + # some times someone comes to signup as a new user, but there is context to preserve. + # the AnyUser cookie is one way that we can track the user from pre-signup to post-signup + # anyway, once the signup is done, we check to see if there is a SignupHint, and if so, + # we use it to figure out what to do with the user after they signup + class SignupHint < ActiveRecord::Base + + belongs_to :jam_track, class_name: 'JamRuby::JamTrack' + + belongs_to :user, class_name: 'JamRuby::User' + + validates :redirect_location, length: {maximum: 1000} + validates :want_jamblaster, inclusion: {in: [nil, true, false]} + + def self.refresh_by_anoymous_user(anonymous_user, options = {}) + + hint = SignupHint.find_by_anonymous_user_id(anonymous_user.id) + + unless hint + hint = SignupHint.new + end + + hint.anonymous_user_id = anonymous_user.id + hint.redirect_location = options[:redirect_location] if options.has_key?(:redirect_location) + hint.want_jamblaster = options[:want_jamblaster] if options.has_key?(:want_jamblaster) + #hint.jam_track = JamTrack.find(options[:jam_track]) if options.has_key?(:jam_track) + hint.expires_at = 15.minutes.from_now + hint.save + hint + end + + def self.delete_old + SignupHint.where("created_at < :week", {:week => 1.week.ago}).delete_all + end + + end +end diff --git a/ruby/lib/jam_ruby/models/track.rb b/ruby/lib/jam_ruby/models/track.rb index e430edb2d..1a8db2047 100644 --- a/ruby/lib/jam_ruby/models/track.rb +++ b/ruby/lib/jam_ruby/models/track.rb @@ -113,6 +113,7 @@ module JamRuby result = {} backing_tracks = [] unless backing_tracks + tracks = [] unless tracks Track.transaction do connection = Connection.find_by_client_id!(clientId) diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 4d9b3b21a..f91973626 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -144,7 +144,7 @@ module JamRuby has_many :event_sessions, :class_name => "JamRuby::EventSession" # affiliate_partner - has_one :affiliate_partner, :class_name => "JamRuby::AffiliatePartner", :foreign_key => :partner_user_id + has_one :affiliate_partner, :class_name => "JamRuby::AffiliatePartner", :foreign_key => :partner_user_id, inverse_of: :partner_user belongs_to :affiliate_referral, :class_name => "JamRuby::AffiliatePartner", :foreign_key => :affiliate_referral_id, :counter_cache => :referral_user_count # diagnostics has_many :diagnostics, :class_name => "JamRuby::Diagnostic" @@ -204,7 +204,7 @@ module JamRuby scope :email_opt_in, where(:subscribe_email => true) def user_progression_fields - @user_progression_fields ||= Set.new ["first_downloaded_client_at", "first_ran_client_at", "first_music_session_at", "first_real_music_session_at", "first_good_music_session_at", "first_certified_gear_at", "first_invited_at", "first_friended_at", "first_recording_at", "first_social_promoted_at" ] + @user_progression_fields ||= Set.new ["first_downloaded_client_at", "first_ran_client_at", "first_music_session_at", "first_real_music_session_at", "first_good_music_session_at", "first_certified_gear_at", "first_invited_at", "first_friended_at", "first_recording_at", "first_social_promoted_at", "first_played_jamtrack_at" ] end def update_progression_field(field_name, time = DateTime.now) @@ -375,6 +375,10 @@ module JamRuby self.purchased_jam_tracks.count end + def sales_count + self.sales.count + end + def joined_score return nil unless has_attribute?(:score) a = read_attribute(:score) @@ -771,7 +775,7 @@ module JamRuby end if user.id != updater_id - raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR + raise JamPermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR end user.easy_save(first_name, last_name, email, password, password_confirmation, musician, gender, @@ -949,6 +953,8 @@ module JamRuby recaptcha_failed = options[:recaptcha_failed] any_user = options[:any_user] reuse_card = options[:reuse_card] + signup_hint = options[:signup_hint] + affiliate_partner = options[:affiliate_partner] user = User.new @@ -1005,7 +1011,14 @@ module JamRuby user.photo_url = photo_url # copy over the shopping cart to the new user, if a shopping cart is provided - user.shopping_carts = any_user.shopping_carts if any_user + if any_user + user.shopping_carts = any_user.shopping_carts + if user.shopping_carts + user.shopping_carts.each do |shopping_cart| + shopping_cart.anonymous_user_id = nil # nil out the anonymous user ID; required for uniqeness constraint on ShoppingCart + end + end + end unless fb_signup.nil? user.update_fb_authorization(fb_signup) @@ -1059,11 +1072,31 @@ module JamRuby user.save + # if the user has just one, free jamtrack in their shopping cart, and it matches the signup hint, then auto-buy it + # only_freebie_in_cart = + # signup_hint && + # signup_hint.jam_track && + # user.shopping_carts.length == 1 && + # user.shopping_carts[0].cart_product == signup_hint.jam_track && + # user.shopping_carts[0].product_info[:free] + # + # if only_freebie_in_cart + # Sale.place_order(user, user.shopping_carts) + # end + user.errors.add("recaptcha", "verification failed") if recaptcha_failed if user.errors.any? raise ActiveRecord::Rollback else + # if the partner ID was present and the partner doesn't already have a user associated, associate this new user with the affiliate partner + if affiliate_partner && affiliate_partner.partner_user.nil? + affiliate_partner.partner_user = user + unless affiliate_partner.save + @@log.error("unable to associate #{user.to_s} with affiliate_partner #{affiliate_partner.id} / #{affiliate_partner.partner_name}") + end + end + if user.affiliate_referral = AffiliatePartner.find_by_id(affiliate_referral_id) user.save end if affiliate_referral_id.present? @@ -1209,8 +1242,8 @@ module JamRuby :cropped_s3_path => cropped_s3_path, :cropped_large_s3_path => cropped_large_s3_path, :crop_selection => crop_selection, - :photo_url => S3Util.url(aws_bucket, escape_filename(cropped_s3_path), :secure => false), - :large_photo_url => S3Util.url(aws_bucket, escape_filename(cropped_large_s3_path), :secure => false) + :photo_url => S3Util.url(aws_bucket, escape_filename(cropped_s3_path), :secure => true), + :large_photo_url => S3Util.url(aws_bucket, escape_filename(cropped_large_s3_path), :secure => true) ) end @@ -1533,6 +1566,61 @@ module JamRuby ShoppingCart.where("user_id=?", self).destroy_all end + def unsubscribe_token + self.class.create_access_token(self) + end + + # Verifier based on our application secret + def self.verifier + ActiveSupport::MessageVerifier.new(APP_CONFIG.secret_token) + end + + # Get a user from a token + def self.read_access_token(signature) + uid = self.verifier.verify(signature) + User.find_by_id uid + rescue ActiveSupport::MessageVerifier::InvalidSignature + nil + end + + # Class method for token generation + def self.create_access_token(user) + verifier.generate(user.id) + end + + # URL to jam-admin + def admin_url + APP_CONFIG.admin_root_url + "/admin/users/" + id + end + + def jam_track_rights_admin_url + APP_CONFIG.admin_root_url + "/admin/jam_track_rights?q[user_id_equals]=#{id}&commit=Filter&order=created_at DESC" + end + + # these are signup attributes that we default to when not presenting the typical form @ /signup + def self.musician_defaults(remote_ip, confirmation_url, any_user, options) + options = options || {} + options[:remote_ip] = remote_ip + options[:birth_date] = nil + options[:instruments] = [{:instrument_id => 'other', :proficiency_level => 1, :priority => 1}] + options[:musician] = true + options[:skip_recaptcha] = true + options[:invited_user] = nil + options[:fb_signup] = nil + options[:signup_confirm_url] = confirmation_url + options[:any_user] = any_user + options + end + + def should_attribute_sale?(shopping_cart) + if affiliate_referral + referral_info = affiliate_referral.should_attribute_sale?(shopping_cart) + else + false + end + + + end private def create_remember_token self.remember_token = SecureRandom.urlsafe_base64 diff --git a/ruby/lib/jam_ruby/mq_router.rb b/ruby/lib/jam_ruby/mq_router.rb index 826eb1ace..8aec53cde 100644 --- a/ruby/lib/jam_ruby/mq_router.rb +++ b/ruby/lib/jam_ruby/mq_router.rb @@ -17,7 +17,7 @@ class MQRouter end if !music_session.access? user - raise PermissionError, 'not allowed to join the specified session' + raise JamPermissionError, 'not allowed to join the specified session' end return music_session diff --git a/ruby/lib/jam_ruby/recurly_client.rb b/ruby/lib/jam_ruby/recurly_client.rb index ce9f44f9f..23dff75c9 100644 --- a/ruby/lib/jam_ruby/recurly_client.rb +++ b/ruby/lib/jam_ruby/recurly_client.rb @@ -1,6 +1,6 @@ require 'recurly' module JamRuby - class RecurlyClient + class RecurlyClient def initialize() @log = Logging.logger[self] end @@ -11,37 +11,37 @@ module JamRuby begin #puts "Recurly.api_key: #{Recurly.api_key}" account = Recurly::Account.create(options) - raise RecurlyClientError.new(account.errors) if account.errors.any? - rescue Recurly::Error, NoMethodError => x + raise RecurlyClientError.new(account.errors) if account.errors.any? + rescue Recurly::Error, NoMethodError => x #puts "Error: #{x} : #{Kernel.caller}" raise RecurlyClientError, x.to_s else if account - current_user.update_attribute(:recurly_code, account.account_code) - end - end - account + current_user.update_attribute(:recurly_code, account.account_code) + end + end + account end def has_account?(current_user) - account = get_account(current_user) + account = get_account(current_user) !!account end def delete_account(current_user) - account = get_account(current_user) - if (account) + account = get_account(current_user) + if (account) begin - account.destroy + account.destroy rescue Recurly::Error, NoMethodError => x raise RecurlyClientError, x.to_s end - else + else raise RecurlyClientError, "Could not find account to delete." end account end - + def get_account(current_user) current_user && current_user.recurly_code ? Recurly::Account.find(current_user.recurly_code) : nil rescue Recurly::Error => x @@ -51,9 +51,9 @@ module JamRuby def update_account(current_user, billing_info=nil) account = get_account(current_user) if(account.present?) - options = account_hash(current_user, billing_info) + options = account_hash(current_user, billing_info) begin - account.update_attributes(options) + account.update_attributes(options) rescue Recurly::Error, NoMethodError => x raise RecurlyClientError, x.to_s end @@ -61,12 +61,20 @@ module JamRuby account end - def payment_history(current_user) + def payment_history(current_user, options ={}) + + limit = params[:limit] + limit ||= 20 + limit = limit.to_i + + cursor = options[:cursor] + payments = [] account = get_account(current_user) if(account.present?) begin - account.transactions.find_each do |transaction| + + account.transaction.paginate(per_page:limit, cursor:cursor).each do |transaction| # XXX this isn't correct because we create 0 dollar transactions too (for free stuff) #if transaction.amount_in_cents > 0 # Account creation adds a transaction record payments << { @@ -74,7 +82,8 @@ module JamRuby :amount_in_cents => transaction.amount_in_cents, :status => transaction.status, :payment_method => transaction.payment_method, - :reference => transaction.reference + :reference => transaction.reference, + :plan_code => transaction.plan_code } #end end @@ -95,7 +104,7 @@ module JamRuby raise RecurlyClientError, x.to_s end - raise RecurlyClientError.new(account.errors) if account.errors.any? + raise RecurlyClientError.new(account.errors) if account.errors.any? else raise RecurlyClientError, "Could not find account to update billing info." end @@ -121,21 +130,21 @@ module JamRuby #puts "subscription.plan.plan_code: #{subscription.plan.plan_code} / #{jam_track.plan_code} / #{subscription.plan.plan_code == jam_track.plan_code}" if(subscription.plan.plan_code == jam_track.plan_code) subscription.terminate(:full) - raise RecurlyClientError.new(subscription.errors) if subscription.errors.any? + raise RecurlyClientError.new(subscription.errors) if subscription.errors.any? terminated = true end - end + end if terminated - jam_track_right.destroy() + jam_track_right.destroy() else raise RecurlyClientError, "Subscription '#{jam_track.plan_code}' not found for this user; could not issue refund." end - + rescue Recurly::Error, NoMethodError => x raise RecurlyClientError, x.to_s end - + else raise RecurlyClientError, "Could not find account to refund order." end @@ -177,89 +186,18 @@ module JamRuby raise RecurlyClientError.new(plan.errors) if plan.errors.any? end - def place_order(current_user, jam_track, shopping_cart, sale) - jam_track_right = nil - account = get_account(current_user) - if (account.present?) - begin - - # see if we can find existing plan for this plan_code, which should occur for previous-in-time error scenarios - recurly_subscription_uuid = nil - account.subscriptions.find_each do |subscription| - if subscription.plan.plan_code == jam_track.plan_code - recurly_subscription_uuid = subscription.uuid - break - end - end - - free = false - - # this means we already have a subscription, so don't try to create a new one for the same plan (Recurly would fail this anyway) - unless recurly_subscription_uuid - - # if the shopping cart was specified, see if the item should be free - free = shopping_cart.nil? ? false : shopping_cart.free? - # and if it's free, squish the charge to 0. - unit_amount_in_cents = free ? 0 : nil - subscription = Recurly::Subscription.create(:account=>account, :plan_code=>jam_track.plan_code, unit_amount_in_cents: unit_amount_in_cents) - - raise RecurlyClientError.new(subscription.errors) if subscription.errors.any? - - # add a line item for the sale - sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, subscription.uuid) - - unless sale_line_item.valid? - @log.error("sale item invalid! #{sale_line_item.errors.inspect}") - puts("sale item invalid! #{sale_line_item.errors.inspect}") - Stats.write('web.recurly.purchase.sale_invalid', {message: sale.errors.to_s, value:1}) - end - - # delete from shopping cart the subscription - shopping_cart.destroy if shopping_cart - - recurly_subscription_uuid = subscription.uuid - end - - #raise RecurlyClientError, "Plan code '#{paid_subscription.plan_code}' doesn't match jam track: '#{jam_track.plan_code}'" unless recurly_subscription_uuid - - jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) do |jam_track_right| - jam_track_right.redeemed = free - end - - # also if the purchase was a free one, then update the user record to no longer allow redeemed jamtracks - User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false) if free - - # this can't go in the block above, as it's here to fix bad subscription UUIDs in an update path - if jam_track_right.recurly_subscription_uuid != recurly_subscription_uuid - jam_track_right.recurly_subscription_uuid = recurly_subscription_uuid - jam_track_right.save - end - - raise RecurlyClientError.new("Error creating jam_track_right for jam_track: #{jam_track.id}") if jam_track_right.nil? - raise RecurlyClientError.new(jam_track_right.errors) if jam_track_right.errors.any? - rescue Recurly::Error, NoMethodError => x - raise RecurlyClientError, x.to_s - end - - raise RecurlyClientError.new(account.errors) if account.errors.any? - else - raise RecurlyClientError, "Could not find account to place order." - end - jam_track_right - end def find_or_create_account(current_user, billing_info) account = get_account(current_user) - + if(account.nil?) account = create_account(current_user, billing_info) else update_billing_info(current_user, billing_info) - end - account + end + account end - - + private def account_hash(current_user, billing_info) options = { @@ -273,8 +211,9 @@ module JamRuby country: current_user.country } } - + options[:billing_info] = billing_info if billing_info + options end end # class @@ -282,11 +221,11 @@ module JamRuby class RecurlyClientError < Exception attr_accessor :errors def initialize(data) - if data.respond_to?('has_key?') - self.errors = data + if data.respond_to?('has_key?') + self.errors = data else self.errors = {:message=>data.to_s} - end + end end # initialize def to_s diff --git a/ruby/lib/jam_ruby/resque/audiomixer.rb b/ruby/lib/jam_ruby/resque/audiomixer.rb index fa987b69f..830726fdd 100644 --- a/ruby/lib/jam_ruby/resque/audiomixer.rb +++ b/ruby/lib/jam_ruby/resque/audiomixer.rb @@ -62,7 +62,6 @@ module JamRuby end - def fetch_audio_files @manifest[:files].each do |file| filename = file[:filename] @@ -73,7 +72,7 @@ module JamRuby uri = URI(filename) open download_filename, 'wb' do |io| begin - Net::HTTP.start(uri.host, uri.port) do |http| + Net::HTTP.start(uri.host, uri.port, use_ssl: filename.start_with?('https') ? true : false) do |http| request = Net::HTTP::Get.new uri http.request request do |response| response_code = response.code.to_i @@ -166,6 +165,7 @@ module JamRuby uri = URI.parse(@postback_ogg_url) http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = @postback_ogg_url.start_with?('https') ? true : false request = Net::HTTP::Put.new(uri.request_uri) response = nil @@ -187,6 +187,7 @@ module JamRuby uri = URI.parse(@postback_mp3_url) http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = @postback_mp3_url.start_with?('https') ? true : false request = Net::HTTP::Put.new(uri.request_uri) response = nil @@ -263,6 +264,7 @@ module JamRuby @@log.debug("manifest") @@log.debug("--------") @@log.debug(JSON.pretty_generate(@manifest)) + @@log.debug("--------") @manifest[:mix_id] = mix_id # slip in the mix_id so that the job can add it to the ogg comments diff --git a/ruby/lib/jam_ruby/resque/google_analytics_event.rb b/ruby/lib/jam_ruby/resque/google_analytics_event.rb index 0c1cbbf06..5394744f2 100644 --- a/ruby/lib/jam_ruby/resque/google_analytics_event.rb +++ b/ruby/lib/jam_ruby/resque/google_analytics_event.rb @@ -101,7 +101,9 @@ module JamRuby el: 'data', ev: data.to_s } + RestClient.post(APP_CONFIG.ga_endpoint, params: params, timeout: 8, open_timeout: 8) + @@log.info("done (#{category}, #{action})") end diff --git a/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb b/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb index 395d12084..359bdc514 100644 --- a/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb +++ b/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb @@ -29,13 +29,28 @@ module JamRuby @jam_track_right = JamTrackRight.find(jam_track_right_id) # bailout check - if @jam_track_right.signed + if @jam_track_right.signed?(bitrate) log.debug("package is already signed. bailing") return end + # compute the step count + total_steps = @jam_track_right.jam_track.stem_tracks.count + 1 # the '1' represents the jkz.py invocation + # track that it's started ( and avoid db validations ) - JamTrackRight.where(:id => @jam_track_right.id).update_all(:signing_started_at => Time.now, :should_retry => false) + signing_started_at = Time.now + signing_started_model_symbol = bitrate == 48 ? :signing_started_at_48 : :signing_started_at_44 + signing_state_symbol = bitrate == 48 ? :signing_48 : :signing_44 + last_step_at = Time.now + JamTrackRight.where(:id => @jam_track_right.id).update_all(signing_started_model_symbol => signing_started_at, :should_retry => false, packaging_steps: total_steps, current_packaging_step: 0, last_step_at: last_step_at, signing_state_symbol => true) + # because we are skipping 'after_save', we have to keep the model current for the notification. A bit ugly... + @jam_track_right.current_packaging_step = 0 + @jam_track_right.packaging_steps = total_steps + @jam_track_right[signing_started_model_symbol] = signing_started_at + @jam_track_right[signing_state_symbol] = true + @jam_track_right.should_retry = false + @jam_track_right.last_step_at = Time.now + SubscriptionMessage.jam_track_signing_job_change(@jam_track_right) JamRuby::JamTracksManager.save_jam_track_right_jkz(@jam_track_right, self.bitrate) # If bitrate is 48 (the default), use that URL. Otherwise, use 44kHz: @@ -62,7 +77,7 @@ module JamRuby @error_reason = "unhandled-job-exception" @error_detail = e.to_s end - @jam_track_right.finish_errored(@error_reason, @error_detail) + @jam_track_right.finish_errored(@error_reason, @error_detail, self.bitrate) rescue Exception => e log.error "unable to post back to the database the error #{e}" diff --git a/ruby/lib/jam_ruby/resque/long_running.rb b/ruby/lib/jam_ruby/resque/long_running.rb new file mode 100644 index 000000000..0ace4abae --- /dev/null +++ b/ruby/lib/jam_ruby/resque/long_running.rb @@ -0,0 +1,25 @@ +require 'resque' +require 'resque-retry' + +module JamRuby + class LongRunning + extend JamRuby::ResqueStats + + attr_accessor :time + + @queue = :long_running + + def model_name + 'long_running' + end + + def log + @log || Logging.logger[LongRunning] + end + + def self.perform(time) + sleep(time) + end + end +end + diff --git a/ruby/lib/jam_ruby/resque/quick_mixer.rb b/ruby/lib/jam_ruby/resque/quick_mixer.rb index 93704173f..4412e0b0c 100644 --- a/ruby/lib/jam_ruby/resque/quick_mixer.rb +++ b/ruby/lib/jam_ruby/resque/quick_mixer.rb @@ -79,6 +79,7 @@ module JamRuby uri = URI.parse(@postback_mp3_url) http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = @postback_mp3_url.start_with?('https') ? true : false request = Net::HTTP::Put.new(uri.request_uri) response = nil diff --git a/ruby/lib/jam_ruby/resque/scheduled/cleanup_facebook_signup.rb b/ruby/lib/jam_ruby/resque/scheduled/cleanup_facebook_signup.rb index ddfb6b28f..f46946886 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/cleanup_facebook_signup.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/cleanup_facebook_signup.rb @@ -11,6 +11,7 @@ module JamRuby @@log.debug("waking up") FacebookSignup.delete_old + SignupHint.delete_old @@log.debug("done") end diff --git a/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb b/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb index cb14d7866..5039fc862 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb @@ -23,10 +23,12 @@ module JamRuby end def perform - JamTrackRight.ready_to_clean.each do |jam_track_right| - log.debug("deleting files for jam_track_right #{jam_track_right.id}") - jam_track_right.delete_s3_files - end + # this needs more testing + return + #JamTrackRight.ready_to_clean.each do |jam_track_right| + # log.debug("deleting files for jam_track_right #{jam_track_right.id}") + # jam_track_right.delete_s3_files + #end end end end diff --git a/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb b/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb index f22dd4bad..6bff4192e 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb @@ -29,6 +29,8 @@ module JamRuby log.debug("starting...") Stats.write('connection', Connection.stats) Stats.write('users', User.stats) + Stats.write('sessions', ActiveMusicSession.stats) + Stats.write('jam_track_rights', JamTrackRight.stats) end end diff --git a/ruby/lib/jam_ruby/resque/scheduled/tally_affiliates.rb b/ruby/lib/jam_ruby/resque/scheduled/tally_affiliates.rb new file mode 100644 index 000000000..2327f281f --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/tally_affiliates.rb @@ -0,0 +1,31 @@ +require 'json' +require 'resque' +require 'resque-retry' +require 'net/http' +require 'digest/md5' + +module JamRuby + + # periodically scheduled to find jobs that need retrying + class TallyAffiliates + extend Resque::Plugins::JamLonelyJob + + @queue = :tally_affiliates + + @@log = Logging.logger[TallyAffiliates] + + def self.lock_timeout + # this should be enough time to make sure the job has finished, but not so long that the system isn't recovering from a abandoned job + 120 + end + + def self.perform + @@log.debug("waking up") + + AffiliatePartner.tally_up(Date.today) + + @@log.debug("done") + end + end + +end \ No newline at end of file diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 296928d8b..68e34d8df 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -761,6 +761,8 @@ FactoryGirl.define do factory :jam_track_right, :class => JamRuby::JamTrackRight do association :jam_track, factory: :jam_track association :user, factory: :user + signing_44 false + signing_48 false end factory :jam_track_tap_in, :class => JamRuby::JamTrackTapIn do @@ -803,4 +805,31 @@ FactoryGirl.define do frequency 3 end + factory :affiliate_partner, class: 'JamRuby::AffiliatePartner' do + sequence(:partner_name) { |n| "partner-#{n}" } + entity_type 'Individual' + signed_at Time.now + association :partner_user, factory: :user + end + + factory :affiliate_quarterly_payment, class: 'JamRuby::AffiliateQuarterlyPayment' do + year 2015 + quarter 0 + association :affiliate_partner, factory: :affiliate_partner + end + + factory :affiliate_monthly_payment, class: 'JamRuby::AffiliateMonthlyPayment' do + year 2015 + month 0 + association :affiliate_partner, factory: :affiliate_partner + end + + factory :affiliate_referral_visit, class: 'JamRuby::AffiliateReferralVisit' do + ip_address '1.1.1.1' + association :affiliate_partner, factory: :affiliate_partner + end + + factory :affiliate_legalese, class: 'JamRuby::AffiliateLegalese' do + legalese Faker::Lorem.paragraphs(6).join("\n\n") + end end diff --git a/ruby/spec/jam_ruby/jam_track_importer_spec.rb b/ruby/spec/jam_ruby/jam_track_importer_spec.rb index 5a0a89cf4..a2cf96620 100644 --- a/ruby/spec/jam_ruby/jam_track_importer_spec.rb +++ b/ruby/spec/jam_ruby/jam_track_importer_spec.rb @@ -46,7 +46,7 @@ describe JamTrackImporter do let(:options) {{ skip_audio_upload:true }} it "bare minimum specification" do - importer.synchronize_metadata(jam_track, minimum_meta, metalocation, 'Artist 1', 'Song 1') + importer.synchronize_metadata(jam_track, minimum_meta, metalocation, 'Artist 1', 'Song 1', options) jam_track.plan_code.should eq('jamtrack-artist1-song1') jam_track.name.should eq("Song 1") @@ -57,7 +57,7 @@ describe JamTrackImporter do jam_track.original_artist.should eq('Artist 1') jam_track.songwriter.should be_nil jam_track.publisher.should be_nil - jam_track.sales_region.should eq('United States') + jam_track.sales_region.should eq('Worldwide') jam_track.price.should eq(1.99) end end diff --git a/ruby/spec/jam_ruby/models/active_music_session_spec.rb b/ruby/spec/jam_ruby/models/active_music_session_spec.rb index c117a65c2..d08299f04 100644 --- a/ruby/spec/jam_ruby/models/active_music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/active_music_session_spec.rb @@ -982,5 +982,70 @@ describe ActiveMusicSession do end + describe "stats" do + it "empty" do + ActiveMusicSession.stats['count'].should eq(0) + end + + + + it "one" do + FactoryGirl.create(:active_music_session) + ActiveMusicSession.stats.should eq('count' => 1, + 'jam_track_count' => 0, + 'backing_track_count' => 0, + 'metronome_count' => 0, + 'recording_count' => 0) + end + + it "two" do + ams1 = FactoryGirl.create(:active_music_session, jam_track_initiator_id: '1') + ams2 = FactoryGirl.create(:active_music_session, jam_track_initiator_id: '2') + user1 = FactoryGirl.create(:user) + user2 = FactoryGirl.create(:user) + ActiveMusicSession.stats.should eq('count' => 2, + 'jam_track_count' => 2, + 'backing_track_count' => 0, + 'metronome_count' => 0, + 'recording_count' => 0) + + ams1.backing_track_initiator = user1 + ams2.backing_track_initiator = user2 + ams1.save! + ams2.save! + + ActiveMusicSession.stats.should eq('count' => 2, + 'jam_track_count' => 2, + 'backing_track_count' => 2, + 'metronome_count' => 0, + 'recording_count' => 0) + + ams1.metronome_initiator = user1 + ams2.metronome_initiator = user2 + ams1.save! + ams2.save! + + ActiveMusicSession.stats.should eq('count' => 2, + 'jam_track_count' => 2, + 'backing_track_count' => 2, + 'metronome_count' => 2, + 'recording_count' => 0) + + ams1.claimed_recording_initiator = user1 + ams2.claimed_recording_initiator = user2 + ams1.save! + ams2.save! + + ActiveMusicSession.stats.should eq('count' => 2, + 'jam_track_count' => 2, + 'backing_track_count' => 2, + 'metronome_count' => 2, + 'recording_count' => 2) + + end + + + end + end diff --git a/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb index 9b25931d9..822a1f527 100644 --- a/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb +++ b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb @@ -3,47 +3,46 @@ require 'spec_helper' describe AffiliatePartner do let!(:user) { FactoryGirl.create(:user) } - let!(:partner) { - AffiliatePartner.create_with_params({:partner_name => 'partner', - :partner_code => 'code', - :user_email => user.email}) - } + let(:partner) { FactoryGirl.create(:affiliate_partner) } + let!(:legalese) { FactoryGirl.create(:affiliate_legalese) } + let(:jam_track) {FactoryGirl.create(:jam_track) } - # Faker::Lorem.word is tripping up the PARTNER_CODE_REGEX. We should not use it. - it 'validates required fields' do - pending - expect(partner.referral_user_count).to eq(0) - expect(partner.partner_user).to eq(user) - user.reload - expect(user.affiliate_partner).to eq(partner) + describe "unpaid" do + it "succeeds with no data" do + AffiliatePartner.unpaid.length.should eq(0) + end - oo = AffiliatePartner.create_with_params({:partner_name => Faker::Company.name, - :partner_code => 'a', - :user_email => user.email}) - expect(oo.errors.messages[:partner_code][0]).to eq('is invalid') - oo = AffiliatePartner.create_with_params({:partner_name => Faker::Company.name, - :partner_code => 'foo bar', - :user_email => user.email}) - expect(oo.errors.messages[:partner_code][0]).to eq('is invalid') - oo = AffiliatePartner.create_with_params({:partner_name => '', - :partner_code => Faker::Lorem.word, - :user_email => user.email}) - expect(oo.errors.messages[:partner_name][0]).to eq("can't be blank") - oo = AffiliatePartner.create_with_params({:partner_name => '', - :partner_code => Faker::Lorem.word, - :user_email => Faker::Internet.email}) - expect(oo.errors.messages[:partner_user][0]).to eq("can't be blank") + it "finds one unpaid partner" do + quarter = FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner: partner, closed:true, paid:false, due_amount_in_cents: AffiliatePartner::PAY_THRESHOLD) + AffiliatePartner.unpaid.should eq([partner]) + end - code = Faker::Lorem.word.upcase - oo = AffiliatePartner.create_with_params({:partner_name => Faker::Company.name, - :partner_code => " #{code} ", - :user_email => user.email}) - expect(oo.partner_code).to eq(code.downcase) + it "finds one unpaid partner with two quarters that exceed threshold" do + # this $5 quarter is not enough to make the threshold + quarter = FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner: partner, year:2016, closed:true, paid:false, due_amount_in_cents: AffiliatePartner::PAY_THRESHOLD / 2) + AffiliatePartner.unpaid.should eq([]) + + # this should get the user over the hump + quarter = FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner: partner, year:2015, closed:true, paid:false, due_amount_in_cents: AffiliatePartner::PAY_THRESHOLD / 2) + AffiliatePartner.unpaid.should eq([partner]) + end + + it "does not find paid or closed quarters" do + quarter = FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner: partner, year:2016, closed:true, paid:true, due_amount_in_cents: AffiliatePartner::PAY_THRESHOLD) + AffiliatePartner.unpaid.should eq([]) + + quarter = FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner: partner, year:2015, closed:false, paid:true, due_amount_in_cents: AffiliatePartner::PAY_THRESHOLD) + AffiliatePartner.unpaid.should eq([]) + end + end + + it "user-partner association" do + user_partner = FactoryGirl.create(:user, affiliate_partner: partner) + user_partner.affiliate_partner.should_not be_nil + user_partner.affiliate_partner.present?.should be_true end it 'has user referrals' do - pending - expect(AffiliatePartner.coded_id(partner.partner_code)).to eq(partner.id) expect(partner.referral_user_count).to eq(0) uu = FactoryGirl.create(:user) uu.affiliate_referral = partner @@ -73,4 +72,766 @@ describe AffiliatePartner do expect(by_date[keys.last]).to eq(2) end + it 'updates address correctly' do + addy = partner.address.clone + addy[AffiliatePartner::KEY_ADDR1] = Faker::Address.street_address + addy[AffiliatePartner::KEY_ADDR2] = Faker::Address.secondary_address + addy[AffiliatePartner::KEY_CITY] = Faker::Address.city + addy[AffiliatePartner::KEY_STATE] = Faker::Address.state_abbr + addy[AffiliatePartner::KEY_COUNTRY] = Faker::Address.country + partner.update_address_value(AffiliatePartner::KEY_ADDR1, addy[AffiliatePartner::KEY_ADDR1]) + partner.update_address_value(AffiliatePartner::KEY_ADDR2, addy[AffiliatePartner::KEY_ADDR2]) + partner.update_address_value(AffiliatePartner::KEY_CITY, addy[AffiliatePartner::KEY_CITY]) + partner.update_address_value(AffiliatePartner::KEY_STATE, addy[AffiliatePartner::KEY_STATE]) + partner.update_address_value(AffiliatePartner::KEY_COUNTRY, addy[AffiliatePartner::KEY_COUNTRY]) + + expect(partner.address[AffiliatePartner::KEY_ADDR1]).to eq(addy[AffiliatePartner::KEY_ADDR1]) + expect(partner.address[AffiliatePartner::KEY_ADDR2]).to eq(addy[AffiliatePartner::KEY_ADDR2]) + expect(partner.address[AffiliatePartner::KEY_CITY]).to eq(addy[AffiliatePartner::KEY_CITY]) + expect(partner.address[AffiliatePartner::KEY_STATE]).to eq(addy[AffiliatePartner::KEY_STATE]) + expect(partner.address[AffiliatePartner::KEY_COUNTRY]).to eq(addy[AffiliatePartner::KEY_COUNTRY]) + end + + it 'associates legalese' do + + end + + describe "should_attribute_sale?" do + + it "user with no affiliate relationship" do + shopping_cart = ShoppingCart.create user, jam_track, 1 + user.should_attribute_sale?(shopping_cart).should be_false + end + + it "user with an affiliate relationship buying a jamtrack" do + user.affiliate_referral = partner + user.save! + shopping_cart = ShoppingCart.create user, jam_track, 1, false + user.should_attribute_sale?(shopping_cart).should eq({fee_in_cents:20}) + end + + it "user with an affiliate relationship redeeming a jamtrack" do + user.affiliate_referral = partner + user.save! + shopping_cart = ShoppingCart.create user, jam_track, 1, true + user.should_attribute_sale?(shopping_cart).should eq({fee_in_cents:0}) + end + + it "user with an expired affiliate relationship redeeming a jamtrack" do + user.affiliate_referral = partner + user.created_at = (365 * 2 + 1).days.ago + user.save! + shopping_cart = ShoppingCart.create user, jam_track, 1, false + user.should_attribute_sale?(shopping_cart).should be_false + end + end + + describe "created_within_affiliate_window" do + it "user created very recently" do + partner.created_within_affiliate_window(user, Time.now).should be_true + end + + it "user created 2 years, 1 day asgo" do + days_future = 365 * 2 + 1 + + partner.created_within_affiliate_window(user, days_future.days.from_now).should be_false + end + end + + describe "tally_up" do + let(:partner1) {FactoryGirl.create(:affiliate_partner)} + let(:partner2) {FactoryGirl.create(:affiliate_partner)} + let(:payment1) {FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner: partner1)} + let(:user_partner1) { FactoryGirl.create(:user, affiliate_referral: partner1)} + let(:user_partner2) { FactoryGirl.create(:user, affiliate_referral: partner2)} + let(:sale) {Sale.create_jam_track_sale(user_partner1)} + + describe "ensure_quarters_exist" do + + it "runs OK with no data" do + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliateQuarterlyPayment.count.should eq(0) + end + + it "creates new slots" do + partner1.touch + partner2.touch + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliateQuarterlyPayment.count.should eq(2) + quarter = partner1.quarters.first + quarter.year.should eq(2015) + quarter.quarter.should eq(0) + quarter = partner2.quarters.first + quarter.year.should eq(2015) + quarter.quarter.should eq(0) + end + + it "creates one slot, ignoring other" do + partner1.touch + partner2.touch + payment1.touch + AffiliateQuarterlyPayment.count.should eq(1) + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliateQuarterlyPayment.count.should eq(2) + quarter = partner1.quarters.first + quarter.year.should eq(2015) + quarter.quarter.should eq(0) + quarter = partner2.quarters.first + quarter.year.should eq(2015) + quarter.quarter.should eq(0) + end + end + + describe "close_quarters" do + it "runs OK with no data" do + AffiliateQuarterlyPayment.count.should eq(0) + AffiliatePartner.close_quarters(2015, 1) + end + + it "ignores current quarter" do + partner1.touch + partner2.touch + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliateQuarterlyPayment.count.should eq(2) + AffiliatePartner.close_quarters(2015, 0) + AffiliateQuarterlyPayment.where(closed: true).count.should eq(0) + end + + it "closes previous quarter" do + partner1.touch + partner2.touch + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliateQuarterlyPayment.count.should eq(2) + AffiliatePartner.close_quarters(2015, 1) + AffiliateQuarterlyPayment.where(closed: true).count.should eq(2) + end + + it "closes previous quarter (edge case)" do + partner1.touch + partner2.touch + AffiliatePartner.ensure_quarters_exist(2014, 3) + AffiliateQuarterlyPayment.count.should eq(2) + AffiliatePartner.close_quarters(2015, 0) + AffiliateQuarterlyPayment.where(closed: true).count.should eq(2) + end + end + + describe "tally_partner_totals" do + it "runs OK with no data" do + AffiliatePartner.tally_partner_totals + AffiliatePartner.count.should eq(0) + end + + it "updates partner when there is no data to tally" do + partner1.touch + partner1.cumulative_earnings_in_cents.should eq(0) + partner1.referral_user_count.should eq(0) + AffiliatePartner.tally_partner_totals + partner1.reload + partner1.cumulative_earnings_in_cents.should eq(0) + partner1.referral_user_count.should eq(0) + end + + it "updates referral_user_count" do + FactoryGirl.create(:user, affiliate_referral: partner1) + AffiliatePartner.tally_partner_totals + partner1.reload + partner1.referral_user_count.should eq(1) + partner1.cumulative_earnings_in_cents.should eq(0) + + FactoryGirl.create(:user, affiliate_referral: partner2) + AffiliatePartner.tally_partner_totals + partner1.reload + partner1.referral_user_count.should eq(1) + partner1.cumulative_earnings_in_cents.should eq(0) + partner2.reload + partner2.referral_user_count.should eq(1) + partner2.cumulative_earnings_in_cents.should eq(0) + + FactoryGirl.create(:user, affiliate_referral: partner2) + AffiliatePartner.tally_partner_totals + partner1.reload + partner1.referral_user_count.should eq(1) + partner1.cumulative_earnings_in_cents.should eq(0) + partner2.reload + partner2.referral_user_count.should eq(2) + partner2.cumulative_earnings_in_cents.should eq(0) + end + + it "updates cumulative_earnings_in_cents" do + FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner:partner1, year:2015, quarter:0, due_amount_in_cents: 0, closed:true, paid:true) + AffiliatePartner.tally_partner_totals + partner1.reload + partner1.referral_user_count.should eq(0) + partner1.cumulative_earnings_in_cents.should eq(0) + + FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner:partner2, year:2015, quarter:0, due_amount_in_cents: 10, closed:true, paid:true) + AffiliatePartner.tally_partner_totals + partner1.reload + partner1.referral_user_count.should eq(0) + partner1.cumulative_earnings_in_cents.should eq(0) + partner2.reload + partner2.referral_user_count.should eq(0) + partner2.cumulative_earnings_in_cents.should eq(10) + + FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner:partner2, year:2015, quarter:1, due_amount_in_cents: 100, closed:true, paid:true) + AffiliatePartner.tally_partner_totals + partner1.reload + partner1.referral_user_count.should eq(0) + partner1.cumulative_earnings_in_cents.should eq(0) + partner2.reload + partner2.referral_user_count.should eq(0) + partner2.cumulative_earnings_in_cents.should eq(110) + + FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner:partner1, year:2015, quarter:1, due_amount_in_cents: 100, closed:true, paid:true) + AffiliatePartner.tally_partner_totals + partner1.reload + partner1.referral_user_count.should eq(0) + partner1.cumulative_earnings_in_cents.should eq(100) + partner2.reload + partner2.referral_user_count.should eq(0) + partner2.cumulative_earnings_in_cents.should eq(110) + + # a paid=false quarterly payment does not yet reflect in cumulative earnings + FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner:partner1, year:2015, quarter:2, due_amount_in_cents: 1000, closed:false, paid:false) + AffiliatePartner.tally_partner_totals + partner1.reload + partner1.referral_user_count.should eq(0) + partner1.cumulative_earnings_in_cents.should eq(100) + partner2.reload + partner2.referral_user_count.should eq(0) + partner2.cumulative_earnings_in_cents.should eq(110) + end + end + + describe "total_quarters" do + it "runs OK with no data" do + AffiliateQuarterlyPayment.count.should eq(0) + AffiliatePartner.total_quarters(2015, 0) + end + + it "totals 0 with no sales data" do + partner1.touch + partner2.touch + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliateQuarterlyPayment.count.should eq(2) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(0) + quarter.last_updated.should_not be_nil + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(0) + quarter.last_updated.should_not be_nil + end + + it "totals with sales data" do + partner1.touch + partner2.touch + + + # create a freebie for partner1 + shopping_cart = ShoppingCart.create user_partner1, jam_track, 1, true + freebie_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + freebie_sale.affiliate_referral_fee_in_cents.should eq(0) + freebie_sale.created_at = Date.new(2015, 1, 1) + freebie_sale.save! + + + + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliateQuarterlyPayment.count.should eq(2) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(0) + quarter.jamtracks_sold.should eq(0) + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(0) + quarter.jamtracks_sold.should eq(0) + + # create a real sale for partner1 + shopping_cart = ShoppingCart.create user_partner1, jam_track, 1, false + real_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + real_sale.affiliate_referral_fee_in_cents.should eq(20) + real_sale.created_at = Date.new(2015, 1, 1) + real_sale.save! + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(20) + quarter.jamtracks_sold.should eq(1) + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(0) + quarter.jamtracks_sold.should eq(0) + + # create a real sale for partner2 + shopping_cart = ShoppingCart.create user_partner2, jam_track, 1, false + real_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + real_sale.affiliate_referral_fee_in_cents.should eq(20) + real_sale.created_at = Date.new(2015, 1, 1) + real_sale.save! + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(20) + quarter.jamtracks_sold.should eq(1) + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(20) + quarter.jamtracks_sold.should eq(1) + + # create a real sale for partner1 + shopping_cart = ShoppingCart.create user_partner1, jam_track, 1, false + real_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + real_sale.affiliate_referral_fee_in_cents.should eq(20) + real_sale.created_at = Date.new(2015, 1, 1) + real_sale.save! + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(40) + quarter.jamtracks_sold.should eq(2) + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(20) + quarter.jamtracks_sold.should eq(1) + + + # create a real sale for a non-affiliated user + shopping_cart = ShoppingCart.create user, jam_track, 1, false + real_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + real_sale.affiliate_referral_fee_in_cents.should be_nil + real_sale.created_at = Date.new(2015, 1, 1) + real_sale.save! + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(40) + quarter.jamtracks_sold.should eq(2) + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(20) + quarter.jamtracks_sold.should eq(1) + + # create a real sale but in previous quarter (should no have effect on the quarter being computed) + shopping_cart = ShoppingCart.create user_partner1, jam_track, 1, false + real_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + real_sale.affiliate_referral_fee_in_cents.should eq(20) + real_sale.created_at = Date.new(2014, 12, 31) + real_sale.save! + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(40) + quarter.jamtracks_sold.should eq(2) + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(20) + quarter.jamtracks_sold.should eq(1) + + # create a real sale but in later quarter (should no have effect on the quarter being computed) + shopping_cart = ShoppingCart.create user_partner1, jam_track, 1, false + real_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + real_sale.affiliate_referral_fee_in_cents.should eq(20) + real_sale.created_at = Date.new(2015, 4, 1) + real_sale.save! + real_sale_later = real_sale + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(40) + quarter.jamtracks_sold.should eq(2) + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(20) + quarter.jamtracks_sold.should eq(1) + + # create a real sale but then refund it + shopping_cart = ShoppingCart.create user_partner1, jam_track, 1, false + real_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + real_sale.affiliate_referral_fee_in_cents.should eq(20) + real_sale.created_at = Date.new(2015, 3, 31) + real_sale.save! + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(60) + quarter.jamtracks_sold.should eq(3) + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(20) + # now refund it + real_sale.affiliate_refunded_at = Date.new(2015, 3, 1) + real_sale.affiliate_refunded = true + real_sale.save! + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(40) + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(20) + quarter.jamtracks_sold.should eq(1) + + + # create the 2nd quarter, which should add up the sale created a few bits up + AffiliatePartner.ensure_quarters_exist(2015, 1) + AffiliatePartner.total_quarters(2015, 1) + payment = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(1, 2015, partner1.id) + payment.due_amount_in_cents.should eq(20) + + # and now refund it in the 3rd quarter + real_sale_later.affiliate_refunded_at = Date.new(2015, 7, 1) + real_sale_later.affiliate_refunded = true + real_sale_later.save! + AffiliatePartner.total_quarters(2015, 1) + payment = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(1, 2015, partner1.id) + payment.due_amount_in_cents.should eq(20) + payment.jamtracks_sold.should eq(1) + + # now catch the one refund in the 3rd quarter + AffiliatePartner.ensure_quarters_exist(2015, 2) + AffiliatePartner.total_quarters(2015, 2) + payment = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(2, 2015, partner1.id) + payment.due_amount_in_cents.should eq(-20) + payment.jamtracks_sold.should eq(-1) + + end + end + + describe "tally_up complete" do + it "runs OK with no data" do + AffiliatePartner.tally_up(Date.new(2015, 1, 1)) + end + + it "successive runs" do + GenericState.singleton.affiliate_tallied_at.should be_nil + AffiliatePartner.tally_up(Date.new(2015, 1, 1)) + GenericState.singleton.affiliate_tallied_at.should_not be_nil + AffiliateQuarterlyPayment.count.should eq(0) + + # partner is created + partner1.touch + + AffiliatePartner.tally_up(Date.new(2015, 1, 1)) + AffiliateQuarterlyPayment.count.should eq(2) + AffiliateMonthlyPayment.count.should eq(6) + + quarter_previous = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(3, 2014, partner1.id) + quarter_previous.due_amount_in_cents.should eq(0) + quarter_previous.closed.should be_true + quarter = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(0, 2015, partner1.id) + quarter.due_amount_in_cents.should eq(0) + quarter.closed.should be_false + + month_previous= AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(10, 2014, partner1.id) + month_previous.due_amount_in_cents.should eq(0) + month_previous.closed.should be_true + month_previous.jamtracks_sold.should eq(0) + month_previous= AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(11, 2014, partner1.id) + month_previous.due_amount_in_cents.should eq(0) + month_previous.closed.should be_true + month_previous.jamtracks_sold.should eq(0) + month_previous= AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(12, 2014, partner1.id) + month_previous.due_amount_in_cents.should eq(0) + month_previous.closed.should be_true + month_previous.jamtracks_sold.should eq(0) + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(1, 2015, partner1.id) + month_previous.due_amount_in_cents.should eq(0) + month_previous.closed.should be_true + month_previous.jamtracks_sold.should eq(0) + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(2, 2015, partner1.id) + month_previous.due_amount_in_cents.should eq(0) + month_previous.closed.should be_true + month.jamtracks_sold.should eq(0) + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(3, 2015, partner1.id) + month_previous.due_amount_in_cents.should eq(0) + month_previous.closed.should be_true + month_previous.jamtracks_sold.should eq(0) + + + shopping_cart = ShoppingCart.create user_partner1, jam_track, 1, false + real_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + real_sale.affiliate_referral_fee_in_cents.should eq(20) + real_sale.created_at = Date.new(2015, 4, 1) + real_sale.save! + + AffiliatePartner.tally_up(Date.new(2015, 4, 1)) + AffiliateQuarterlyPayment.count.should eq(3) + quarter = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(0, 2015, partner1.id) + quarter.due_amount_in_cents.should eq(0) + quarter.jamtracks_sold.should eq(0) + quarter.closed.should be_true + quarter2 = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(1, 2015, partner1.id) + quarter2.due_amount_in_cents.should eq(20) + quarter2.jamtracks_sold.should eq(1) + quarter2.closed.should be_false + + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(1, 2015, partner1.id) + month.due_amount_in_cents.should eq(0) + month.jamtracks_sold.should eq(0) + month.closed.should be_true + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(2, 2015, partner1.id) + month.due_amount_in_cents.should eq(0) + month.jamtracks_sold.should eq(0) + month.closed.should be_true + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(3, 2015, partner1.id) + month.due_amount_in_cents.should eq(0) + month.jamtracks_sold.should eq(0) + month.closed.should be_true + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(4, 2015, partner1.id) + month.due_amount_in_cents.should eq(20) + month.jamtracks_sold.should eq(1) + month.closed.should be_false + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(5, 2015, partner1.id) + month.due_amount_in_cents.should eq(0) + month.jamtracks_sold.should eq(0) + month.closed.should be_false + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(6, 2015, partner1.id) + month.due_amount_in_cents.should eq(0) + month.jamtracks_sold.should eq(0) + month.closed.should be_false + + # now sneak in a purchase in the 1st quarter, which makes no sense, but proves that closed quarters are not touched + + shopping_cart = ShoppingCart.create user_partner1, jam_track, 1, false + real_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + real_sale.affiliate_referral_fee_in_cents.should eq(20) + real_sale.created_at = Date.new(2015, 1, 1) + real_sale.save! + + AffiliatePartner.tally_up(Date.new(2015, 4, 2)) + quarter = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(0, 2015, partner1.id) + quarter.due_amount_in_cents.should eq(0) + quarter.jamtracks_sold.should eq(0) + quarter.closed.should be_true + + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(1, 2015, partner1.id) + month.due_amount_in_cents.should eq(0) + month.closed.should be_true + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(2, 2015, partner1.id) + month.due_amount_in_cents.should eq(0) + month.jamtracks_sold.should eq(0) + month.closed.should be_true + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(3, 2015, partner1.id) + month.due_amount_in_cents.should eq(0) + month.jamtracks_sold.should eq(0) + month.closed.should be_true + end + end + + describe "tally_traffic_totals" do + + it "runs OK with no data" do + AffiliatePartner.tally_traffic_totals(Date.yesterday, Date.today) + end + + it "can deal with simple signup case" do + user_partner1.touch + + day0 = user_partner1.created_at.to_date + + # simulate what happens when this scheduled job first ever runs + AffiliatePartner.tally_traffic_totals(nil, day0) + AffiliateTrafficTotal.count.should eq(1) + traffic_total_day_before = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0 - 1, user_partner1.affiliate_referral_id) + traffic_total_day_before.visits.should eq(0) + traffic_total_day_before.signups.should eq(0) + + # then simulate when it runs on the same day as it ran on the day before + AffiliatePartner.tally_traffic_totals(day0, day0) + AffiliateTrafficTotal.count.should eq(1) + traffic_total_day_before = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0 - 1, user_partner1.affiliate_referral_id) + traffic_total_day_before.visits.should eq(0) + traffic_total_day_before.signups.should eq(0) + + # now run it on the next day, which should catch the signup event + + day1 = day0 + 1 + AffiliatePartner.tally_traffic_totals(day0, day1) + AffiliateTrafficTotal.count.should eq(2) + traffic_total_day_before = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0 - 1, user_partner1.affiliate_referral_id) + traffic_total_day_before.visits.should eq(0) + traffic_total_day_before.signups.should eq(0) + traffic_total_day0 = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0, user_partner1.affiliate_referral_id) + traffic_total_day0.visits.should eq(0) + traffic_total_day0.signups.should eq(1) + + # add in a visit + visit = FactoryGirl.create(:affiliate_referral_visit, affiliate_partner: user_partner1.affiliate_referral) + + # it won't get seen though because we've moved on + AffiliatePartner.tally_traffic_totals(day1, day1) + AffiliateTrafficTotal.count.should eq(2) + traffic_total_day_before = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0 - 1, user_partner1.affiliate_referral_id) + traffic_total_day_before.visits.should eq(0) + traffic_total_day_before.signups.should eq(0) + traffic_total_day0 = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0, user_partner1.affiliate_referral_id) + traffic_total_day0.visits.should eq(0) + traffic_total_day0.signups.should eq(1) + + # manipulate the visit created_at so we can record it + visit.created_at = day1 + visit.save! + day2 = day1 + 1 + AffiliatePartner.tally_traffic_totals(day1, day2) + AffiliateTrafficTotal.count.should eq(3) + traffic_total_day_before = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0 - 1, user_partner1.affiliate_referral_id) + traffic_total_day_before.visits.should eq(0) + traffic_total_day_before.signups.should eq(0) + traffic_total_day0 = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0, user_partner1.affiliate_referral_id) + traffic_total_day0.visits.should eq(0) + traffic_total_day0.signups.should eq(1) + traffic_total_day1 = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day1, user_partner1.affiliate_referral_id) + traffic_total_day1.visits.should eq(1) + traffic_total_day1.signups.should eq(0) + + # now create 2 records on day 2 for visits and signups both, and a partner with their own visit and signup, and do a final check + + user_partner2.touch + + visit2 = FactoryGirl.create(:affiliate_referral_visit, affiliate_partner: user_partner1.affiliate_referral) + visit3 = FactoryGirl.create(:affiliate_referral_visit, affiliate_partner: user_partner1.affiliate_referral) + visit_partner2 = FactoryGirl.create(:affiliate_referral_visit, affiliate_partner: user_partner2.affiliate_referral) + visit2.created_at = day2 + visit3.created_at = day2 + visit_partner2.created_at = day2 + visit2.save! + visit3.save! + visit_partner2.save! + + user2 = FactoryGirl.create(:user, affiliate_referral:user_partner1.affiliate_referral) + user3 = FactoryGirl.create(:user, affiliate_referral:user_partner1.affiliate_referral) + user2.created_at = day2 + user3.created_at = day2 + user_partner2.created_at = day2 + user2.save! + user3.save! + user_partner2.save! + + + day3 = day2 + 1 + AffiliatePartner.tally_traffic_totals(day2, day3) + AffiliateTrafficTotal.count.should eq(5) + traffic_total_day_before = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0 - 1, user_partner1.affiliate_referral_id) + traffic_total_day_before.visits.should eq(0) + traffic_total_day_before.signups.should eq(0) + traffic_total_day0 = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0, user_partner1.affiliate_referral_id) + traffic_total_day0.visits.should eq(0) + traffic_total_day0.signups.should eq(1) + traffic_total_day1 = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day1, user_partner1.affiliate_referral_id) + traffic_total_day1.visits.should eq(1) + traffic_total_day1.signups.should eq(0) + traffic_total_day2 = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day2, user_partner1.affiliate_referral_id) + traffic_total_day2.visits.should eq(2) + traffic_total_day2.signups.should eq(2) + traffic_total_day2 = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day2, user_partner2.affiliate_referral_id) + traffic_total_day2.visits.should eq(1) + traffic_total_day2.signups.should eq(1) + end + end + + describe "boundary_dates" do + it "1st quarter" do + start_date, end_date = AffiliatePartner.boundary_dates(2015, 0) + start_date.should eq(Date.new(2015, 1, 1)) + end_date.should eq(Date.new(2015, 3, 31)) + end + + it "2nd quarter" do + start_date, end_date = AffiliatePartner.boundary_dates(2015, 1) + start_date.should eq(Date.new(2015, 4, 1)) + end_date.should eq(Date.new(2015, 6, 30)) + end + + it "3rd quarter" do + start_date, end_date = AffiliatePartner.boundary_dates(2015, 2) + start_date.should eq(Date.new(2015, 7, 1)) + end_date.should eq(Date.new(2015, 9, 30)) + end + + it "4th quarter" do + start_date, end_date = AffiliatePartner.boundary_dates(2015, 3) + start_date.should eq(Date.new(2015, 10, 1)) + end_date.should eq(Date.new(2015, 12, 31)) + end + end + + describe "boundary_dates_for_month" do + it "invalid month" do + expect{AffiliatePartner.boundary_dates_for_month(2015, 0)}.to raise_error + end + + it "January" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 1) + start_date.should eq(Date.new(2015, 1, 1)) + end_date.should eq(Date.new(2015, 1, 31)) + end + + it "February" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 2) + start_date.should eq(Date.new(2015, 2, 1)) + end_date.should eq(Date.new(2015, 2, 28)) + end + + it "March" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 3) + start_date.should eq(Date.new(2015, 3, 1)) + end_date.should eq(Date.new(2015, 3, 31)) + end + + it "April" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 4) + start_date.should eq(Date.new(2015, 4, 1)) + end_date.should eq(Date.new(2015, 4, 30)) + end + + it "May" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 5) + start_date.should eq(Date.new(2015, 5, 1)) + end_date.should eq(Date.new(2015, 5, 31)) + end + + it "June" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 6) + start_date.should eq(Date.new(2015, 6, 1)) + end_date.should eq(Date.new(2015, 6, 30)) + end + + it "July" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 7) + start_date.should eq(Date.new(2015, 7, 1)) + end_date.should eq(Date.new(2015, 7, 31)) + end + + it "August" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 8) + start_date.should eq(Date.new(2015, 8, 1)) + end_date.should eq(Date.new(2015, 8, 31)) + end + + it "September" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 9) + start_date.should eq(Date.new(2015, 9, 1)) + end_date.should eq(Date.new(2015, 9, 30)) + end + + it "October" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 10) + start_date.should eq(Date.new(2015, 10, 1)) + end_date.should eq(Date.new(2015, 10, 31)) + end + + it "November" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 11) + start_date.should eq(Date.new(2015, 11, 1)) + end_date.should eq(Date.new(2015, 11, 30)) + end + + it "December" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 12) + start_date.should eq(Date.new(2015, 12, 1)) + end_date.should eq(Date.new(2015, 12, 31)) + end + + it "February in a leap year" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2016, 2) + start_date.should eq(Date.new(2016, 2, 1)) + end_date.should eq(Date.new(2016, 2, 29)) + end + end + end + end diff --git a/ruby/spec/jam_ruby/models/affiliate_payment_spec.rb b/ruby/spec/jam_ruby/models/affiliate_payment_spec.rb new file mode 100644 index 000000000..14f9f4c13 --- /dev/null +++ b/ruby/spec/jam_ruby/models/affiliate_payment_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe AffiliatePayment do + + let(:partner) { FactoryGirl.create(:affiliate_partner) } + let(:user_partner) { FactoryGirl.create(:user, affiliate_partner: partner) } + + it "succeeds with no data" do + results, nex = AffiliatePayment.index(user_partner, {}) + results.length.should eq(0) + end + + it "sorts month and quarters correctly" do + monthly1 = FactoryGirl.create(:affiliate_monthly_payment, affiliate_partner: partner, closed: true, due_amount_in_cents: 10, month: 1, year: 2015) + monthly2 = FactoryGirl.create(:affiliate_monthly_payment, affiliate_partner: partner, closed: true, due_amount_in_cents: 20, month: 2, year: 2015) + monthly3 = FactoryGirl.create(:affiliate_monthly_payment, affiliate_partner: partner, closed: true, due_amount_in_cents: 30, month: 3, year: 2015) + monthly4 = FactoryGirl.create(:affiliate_monthly_payment, affiliate_partner: partner, closed: true, due_amount_in_cents: 40, month: 4, year: 2015) + quarterly = FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner: partner, closed: true, paid:true, due_amount_in_cents: 50, quarter: 0, year: 2015) + results, nex = AffiliatePayment.index(user_partner, {}) + results.length.should eq(5) + result1 = results[0] + result2 = results[1] + result3 = results[2] + result4 = results[3] + result5 = results[4] + + result1.payment_type.should eq('monthly') + result1.due_amount_in_cents.should eq(10) + result2.payment_type.should eq('monthly') + result2.due_amount_in_cents.should eq(20) + result3.payment_type.should eq('monthly') + result3.due_amount_in_cents.should eq(30) + result4.payment_type.should eq('quarterly') + result4.due_amount_in_cents.should eq(50) + result5.payment_type.should eq('monthly') + result5.due_amount_in_cents.should eq(40) + end + +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/affiliate_referral_visit_spec.rb b/ruby/spec/jam_ruby/models/affiliate_referral_visit_spec.rb new file mode 100644 index 000000000..467127500 --- /dev/null +++ b/ruby/spec/jam_ruby/models/affiliate_referral_visit_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe AffiliateReferralVisit do + + let!(:user) { FactoryGirl.create(:user) } + let(:partner) { FactoryGirl.create(:affiliate_partner) } + let(:valid_track_options) { + { + affiliate_id: partner.id, + visited: false, + remote_ip: '1.2.2.1', + visited_url: '/', + referral_url: 'http://www.youtube.com', + current_user: nil + + } + } + + describe "track" do + it "succeeds" do + visit = AffiliateReferralVisit.track( valid_track_options ) + visit.valid?.should be_true + end + + it "never fails with error" do + visit = AffiliateReferralVisit.track( {}) + visit.valid?.should be_false + + options = valid_track_options + options[:affiliate_id] = 111 + visit = AffiliateReferralVisit.track( options) + visit.valid?.should be_true + + options = valid_track_options + options[:current_user] = user + visit = AffiliateReferralVisit.track( options) + visit.valid?.should be_true + end + end +end diff --git a/ruby/spec/jam_ruby/models/feed_spec.rb b/ruby/spec/jam_ruby/models/feed_spec.rb index 550802c8b..fa4372a58 100644 --- a/ruby/spec/jam_ruby/models/feed_spec.rb +++ b/ruby/spec/jam_ruby/models/feed_spec.rb @@ -54,6 +54,10 @@ describe Feed do end describe "sorting" do + before :each do + ClaimedRecording.delete_all + end + it "sorts by active flag / index (date) DESC" do claimed_recording = FactoryGirl.create(:claimed_recording) @@ -74,20 +78,22 @@ describe Feed do FactoryGirl.create(:playable_play, playable: claimed_recording2.recording, claimed_recording: claimed_recording2, user:claimed_recording1.user) FactoryGirl.create(:playable_play, playable: claimed_recording2.recording, claimed_recording: claimed_recording2, user:claimed_recording2.user) + claimed_recording2.recording.play_count += 2 + claimed_recording2.recording.save! feeds, next_page = Feed.index(user1, :sort => 'plays') feeds.length.should == 4 feeds[2].recording.should == claimed_recording2.recording feeds[3].recording.should == claimed_recording1.recording - FactoryGirl.create(:playable_play, playable: claimed_recording1.recording.music_session.music_session, user: user1) - FactoryGirl.create(:playable_play, playable: claimed_recording1.recording.music_session.music_session, user: user2) - FactoryGirl.create(:playable_play, playable: claimed_recording1.recording.music_session.music_session, user: user3) - + FactoryGirl.create(:playable_play, playable: claimed_recording2.recording.music_session.music_session, user: user1) + FactoryGirl.create(:playable_play, playable: claimed_recording2.recording.music_session.music_session, user: user2) + FactoryGirl.create(:playable_play, playable: claimed_recording2.recording.music_session.music_session, user: user3) + claimed_recording2.recording.play_count+=3 + claimed_recording2.recording.save! feeds, next_page = Feed.index(user1, :sort => 'plays') feeds.length.should == 4 - feeds[0].music_session.should == claimed_recording1.recording.music_session.music_session feeds[2].recording.should == claimed_recording2.recording feeds[3].recording.should == claimed_recording1.recording 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 index 530535f8c..9119bdfd9 100644 --- a/ruby/spec/jam_ruby/models/jam_track_right_spec.rb +++ b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb @@ -76,7 +76,7 @@ describe JamTrackRight do JamRuby::JamTracksManager.save_jam_track_jkz(user, jam_track) #}.to_not raise_error(ArgumentError) jam_track_right.reload - jam_track_right[:url_48].should == jam_track_right.store_dir + '/' + jam_track_right.filename + jam_track_right[:url_48].should == jam_track_right.store_dir + '/' + jam_track_right.filename(:url_48) # verify it's on S3 url = jam_track_right[:url_48] @@ -109,11 +109,12 @@ describe JamTrackRight do end it "valid track with rights to it by querying user" do - jam_track_right = FactoryGirl.create(:jam_track_right, private_key: 'keyabc') + jam_track_right = FactoryGirl.create(:jam_track_right, private_key_44: 'keyabc') keys = JamTrackRight.list_keys(jam_track_right.user, [jam_track_right.jam_track.id]) keys.should have(1).items keys[0].id.should == jam_track_right.jam_track.id - keys[0]['private_key'].should eq('keyabc') + keys[0]['private_key_44'].should eq('keyabc') + keys[0]['private_key_48'].should be_nil end end @@ -124,7 +125,7 @@ describe JamTrackRight do end it "signed" do - right = FactoryGirl.create(:jam_track_right, signed: true) + right = FactoryGirl.create(:jam_track_right, signed_44: true, signing_started_at_44: Time.now) right.signing_state.should eq('SIGNED') end @@ -134,23 +135,252 @@ describe JamTrackRight do end it "signing" do - right = FactoryGirl.create(:jam_track_right, signing_started_at: Time.now) - right.signing_state.should eq('SIGNING') + right = FactoryGirl.create(:jam_track_right, signing_started_at_44: Time.now, packaging_steps: 3, current_packaging_step:0, last_step_at:Time.now) + right.signing_state(44).should eq('SIGNING') end it "signing timeout" do - right = FactoryGirl.create(:jam_track_right, signing_started_at: Time.now - (APP_CONFIG.signing_job_run_max_time + 1)) - right.signing_state.should eq('SIGNING_TIMEOUT') + right = FactoryGirl.create(:jam_track_right, signing_started_at_48: Time.now - 100, packaging_steps: 3, current_packaging_step:0, last_step_at:Time.now) + right.signing_state(48).should eq('SIGNING_TIMEOUT') end it "queued" do right = FactoryGirl.create(:jam_track_right, signing_queued_at: Time.now) - right.signing_state.should eq('QUEUED') + right.signing_state(44).should eq('QUEUED') end it "signing timeout" do right = FactoryGirl.create(:jam_track_right, signing_queued_at: Time.now - (APP_CONFIG.signing_job_queue_max_time + 1)) - right.signing_state.should eq('QUEUED_TIMEOUT') + right.signing_state(44).should eq('QUEUED_TIMEOUT') + end + end + + describe "stats" do + + it "empty" do + JamTrackRight.stats['count'].should eq(0) + end + + + + it "one" do + right1 = FactoryGirl.create(:jam_track_right) + JamTrackRight.stats.should eq('count' => 1, + 'signing_count' => 0, + 'redeemed_count' => 0, + 'purchased_count' => 1, + 'redeemed_and_dl_count' => 0) + end + + it "two" do + right1 = FactoryGirl.create(:jam_track_right) + right2 = FactoryGirl.create(:jam_track_right) + + JamTrackRight.stats.should eq('count' => 2, + 'signing_count' => 0, + 'redeemed_count' => 0, + 'purchased_count' => 2, + 'redeemed_and_dl_count' => 0) + + right1.signing_44 = true + right1.save! + right2.signing_48 = true + right2.save! + + JamTrackRight.stats.should eq('count' => 2, + 'signing_count' => 2, + 'redeemed_count' => 0, + 'purchased_count' => 2, + 'redeemed_and_dl_count' => 0) + + right1.redeemed = true + right1.save! + + JamTrackRight.stats.should eq('count' => 2, + 'signing_count' => 2, + 'redeemed_count' => 1, + 'purchased_count' => 1, + 'redeemed_and_dl_count' => 0) + + right2.is_test_purchase = true + right2.save! + + JamTrackRight.stats.should eq('count' => 1, + 'signing_count' => 1, + 'redeemed_count' => 1, + 'purchased_count' => 0, + 'redeemed_and_dl_count' => 0) + end + end + + describe "guard_against_fraud" do + let(:user) {FactoryGirl.create(:user)} + let(:other) {FactoryGirl.create(:user)} + let(:first_fingerprint) { {all: 'all', running: 'running' } } + let(:new_fingerprint) { {all: 'all_2', running: 'running' } } + let(:full_fingerprint) { {all: :all_3, running: :running_3, all_3: { mac: "72:00:02:4C:1E:61", name: "en2", upstate: true }, running_3: { mac: "72:00:02:4C:1E:62", name: "en3", upstate: false } } } + let(:remote_ip) {'1.1.1.1'} + let(:remote_ip2) {'2.2.2.2'} + let(:jam_track_right) { FactoryGirl.create(:jam_track_right, user: user, redeemed: true, redeemed_and_fingerprinted: false) } + let(:jam_track_right_purchased) { FactoryGirl.create(:jam_track_right, user: user, redeemed: false, redeemed_and_fingerprinted: false) } + let(:jam_track_right_other) { FactoryGirl.create(:jam_track_right, user: other, redeemed: true, redeemed_and_fingerprinted: false) } + let(:jam_track_right_other_purchased) { FactoryGirl.create(:jam_track_right, user: other, redeemed: false, redeemed_and_fingerprinted: false) } + + it "denies no current_user" do + jam_track_right.guard_against_fraud(nil, first_fingerprint, remote_ip).should eq('no user specified') + end + + it "denies no fingerprint" do + jam_track_right.guard_against_fraud(user, nil, remote_ip).should eq('no fingerprint specified') + end + + it "allows redemption (success)" do + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil + jam_track_right.valid?.should be_true + jam_track_right.redeemed_and_fingerprinted.should be_true + + + mf = MachineFingerprint.find_by_fingerprint('all') + mf.user.should eq(user) + mf.fingerprint.should eq('all') + mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD) + mf.print_type.should eq(MachineFingerprint::PRINT_TYPE_ALL) + mf.jam_track_right.should eq(jam_track_right) + + mf = MachineFingerprint.find_by_fingerprint('running') + mf.user.should eq(user) + mf.fingerprint.should eq('running') + mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD) + mf.print_type.should eq(MachineFingerprint::PRINT_TYPE_ACTIVE) + mf.jam_track_right.should eq(jam_track_right) + end + + it "ignores already successfully redeemed" do + jam_track_right.redeemed_and_fingerprinted = true + jam_track_right.save! + + jam_track_right.guard_against_fraud(user, new_fingerprint, remote_ip).should be_nil + jam_track_right.valid?.should be_true + + # and no new fingerprints + MachineFingerprint.count.should eq(0) + end + + it "ignores already normally purchased" do + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip) + MachineFingerprint.count.should eq(2) + + jam_track_right_purchased.guard_against_fraud(user, new_fingerprint, remote_ip).should be_nil + jam_track_right_purchased.valid?.should be_true + jam_track_right_purchased.redeemed_and_fingerprinted.should be_false # fingerprint should not be set on normal purchase + + jam_track_right.redeemed_and_fingerprinted.should be_true # should still be redeemed_and fingerprinted; just checking for weird side-effects + + # no new fingerprints + MachineFingerprint.count.should eq(2) + end + + it "protects against re-using fingerprint across users (conflicts on all fp)" do + first_fingerprint2 = first_fingerprint.clone + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil + MachineFingerprint.count.should eq(2) + first_fingerprint2[:running] = 'running_2' + jam_track_right_other.guard_against_fraud(other, first_fingerprint2, remote_ip).should eq("other user has 'all' fingerprint") + + mf = MachineFingerprint.find_by_fingerprint('running') + mf.user.should eq(user) + mf.fingerprint.should eq('running') + mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD) + mf.print_type.should eq(MachineFingerprint::PRINT_TYPE_ACTIVE) + mf.jam_track_right.should eq(jam_track_right) + MachineFingerprint.count.should eq(4) + end + + it "protects against re-using fingerprint across users (conflicts on running fp)" do + first_fingerprint2 = first_fingerprint.clone + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil + MachineFingerprint.count.should eq(2) + first_fingerprint2[:all] = 'all_2' + jam_track_right_other.guard_against_fraud(other, first_fingerprint2, remote_ip).should eq("other user has 'running' fingerprint") + + mf = MachineFingerprint.find_by_fingerprint('all') + mf.user.should eq(user) + mf.fingerprint.should eq('all') + mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD) + mf.print_type.should eq(MachineFingerprint::PRINT_TYPE_ALL) + mf.jam_track_right.should eq(jam_track_right) + MachineFingerprint.count.should eq(4) + + FraudAlert.count.should eq(1) + fraud = FraudAlert.first + fraud.user.should eq(other) + fraud.machine_fingerprint.should eq(MachineFingerprint.where(fingerprint:'running').where(user_id:other.id).first) + end + + it "ignores whitelisted fingerprint" do + whitelist = FingerprintWhitelist.new + whitelist.fingerprint = first_fingerprint[:running] + whitelist.save! + + first_fingerprint2 = first_fingerprint.clone + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil + MachineFingerprint.count.should eq(2) + first_fingerprint2[:all] = 'all_2' + jam_track_right_other.guard_against_fraud(other, first_fingerprint2, remote_ip).should be_nil + + FraudAlert.count.should eq(0) + end + + it "does not conflict if same mac, but different IP address" do + first_fingerprint2 = first_fingerprint.clone + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil + MachineFingerprint.count.should eq(2) + first_fingerprint2[:all] = 'all_2' + jam_track_right_other.guard_against_fraud(other, first_fingerprint2, remote_ip2).should eq(nil) + + mf = MachineFingerprint.find_by_fingerprint('all') + mf.user.should eq(user) + mf.fingerprint.should eq('all') + mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD) + mf.print_type.should eq(MachineFingerprint::PRINT_TYPE_ALL) + mf.jam_track_right.should eq(jam_track_right) + MachineFingerprint.count.should eq(4) + + FraudAlert.count.should eq(0) + end + + # if you try to buy a regular jamtrack with a fingerprint belonging to another user? so what. you paid for it + it "allows re-use of fingerprint if jamtrack is a normal purchase" do + first_fingerprint2 = first_fingerprint.clone + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil + MachineFingerprint.count.should eq(2) + jam_track_right_other_purchased.guard_against_fraud(other, first_fingerprint2, remote_ip).should be_nil + MachineFingerprint.count.should eq(2) + end + + it "stops you from redeeming two jamtracks" do + right1 = FactoryGirl.create(:jam_track_right, user: user, redeemed: true, redeemed_and_fingerprinted: true) + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should eq('already redeemed another') + MachineFingerprint.count.should eq(0) + end + + it "let's you download a free jamtrack if you have a second but undownloaded free one" do + right1 = FactoryGirl.create(:jam_track_right, user: user, redeemed: true, redeemed_and_fingerprinted: false) + first_fingerprint2 = first_fingerprint.clone + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil + MachineFingerprint.count.should eq(2) + + right1.guard_against_fraud(user, first_fingerprint2, remote_ip).should eq('already redeemed another') + MachineFingerprint.count.should eq(2) + end + + it "creates metadata" do + right1 = FactoryGirl.create(:jam_track_right, user: user, redeemed: true, redeemed_and_fingerprinted: false) + + jam_track_right.guard_against_fraud(user, full_fingerprint, remote_ip).should be_nil + MachineFingerprint.count.should eq(2) + MachineExtra.count.should eq(2) + end end diff --git a/ruby/spec/jam_ruby/models/jam_track_spec.rb b/ruby/spec/jam_ruby/models/jam_track_spec.rb index 10cec40cd..d17019df7 100644 --- a/ruby/spec/jam_ruby/models/jam_track_spec.rb +++ b/ruby/spec/jam_ruby/models/jam_track_spec.rb @@ -38,6 +38,41 @@ describe JamTrack do end end + describe "artist_index" do + before :each do + JamTrack.delete_all + end + + it "empty query" do + query, pager = JamTrack.artist_index({}, user) + query.size.should == 0 + end + + it "groups" do + jam_track1 = FactoryGirl.create(:jam_track_with_tracks, original_artist: 'artist', name: 'a') + jam_track2 = FactoryGirl.create(:jam_track_with_tracks, original_artist: 'artist', name: 'b') + + query, pager = JamTrack.artist_index({}, user) + query.size.should == 1 + + query[0].original_artist.should eq('artist') + query[0]['song_count'].should eq('2') + end + + it "sorts by name" do + jam_track1 = FactoryGirl.create(:jam_track_with_tracks, original_artist: 'blartist', name: 'a') + jam_track2 = FactoryGirl.create(:jam_track_with_tracks, original_artist: 'artist', name: 'b') + + query, pager = JamTrack.artist_index({}, user) + query.size.should == 2 + + query[0].original_artist.should eq('artist') + query[0]['song_count'].should eq('1') + query[1].original_artist.should eq('blartist') + query[1]['song_count'].should eq('1') + end + end + describe "index" do it "empty query" do query, pager = JamTrack.index({}, user) @@ -45,8 +80,8 @@ describe JamTrack do end it "sorts by name" do - jam_track1 = FactoryGirl.create(:jam_track_with_tracks, name: 'a') - jam_track2 = FactoryGirl.create(:jam_track_with_tracks, name: 'b') + jam_track1 = FactoryGirl.create(:jam_track_with_tracks, original_artist: 'artist', name: 'a') + jam_track2 = FactoryGirl.create(:jam_track_with_tracks, original_artist: 'artist', name: 'b') query, pager = JamTrack.index({}, user) query.size.should == 2 @@ -141,5 +176,4 @@ describe JamTrack do end end end -end - +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/payment_history_spec.rb b/ruby/spec/jam_ruby/models/payment_history_spec.rb new file mode 100644 index 000000000..b28cffec3 --- /dev/null +++ b/ruby/spec/jam_ruby/models/payment_history_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe PaymentHistory do + + let(:user) {FactoryGirl.create(:user)} + let(:user2) {FactoryGirl.create(:user)} + let(:jam_track) {FactoryGirl.create(:jam_track)} + + before(:each) do + + end + + describe "index" do + it "empty" do + result = PaymentHistory.index(user) + result[:query].length.should eq(0) + result[:next].should eq(nil) + end + + it "one" do + sale = Sale.create_jam_track_sale(user) + shopping_cart = ShoppingCart.create(user, jam_track) + sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_adjustment_uuid', nil) + + result = PaymentHistory.index(user) + result[:query].length.should eq(1) + result[:next].should eq(nil) + end + + it "user filtered correctly" do + sale = Sale.create_jam_track_sale(user) + shopping_cart = ShoppingCart.create(user, jam_track) + sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_adjustment_uuid', nil) + + result = PaymentHistory.index(user) + result[:query].length.should eq(1) + result[:next].should eq(nil) + + sale2 = Sale.create_jam_track_sale(user2) + shopping_cart = ShoppingCart.create(user2, jam_track) + sale_line_item2 = SaleLineItem.create_from_shopping_cart(sale2, shopping_cart, nil, 'some_adjustment_uuid', nil) + + result = PaymentHistory.index(user) + result[:query].length.should eq(1) + result[:next].should eq(nil) + end + end +end diff --git a/ruby/spec/jam_ruby/models/recording_spec.rb b/ruby/spec/jam_ruby/models/recording_spec.rb index 7b64b2144..3de7d09e1 100644 --- a/ruby/spec/jam_ruby/models/recording_spec.rb +++ b/ruby/spec/jam_ruby/models/recording_spec.rb @@ -15,6 +15,29 @@ describe Recording do @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) end + describe "popular_recordings" do + it "empty" do + Recording.popular_recordings.length.should eq(0) + end + + it "one public recording" do + claim = FactoryGirl.create(:claimed_recording) + + claim.recording.is_done = true + claim.recording.save! + recordings = Recording.popular_recordings + recordings.length.should eq(1) + recordings[0].id.should eq(claim.recording.id) + end + + it "one private recording" do + claim = FactoryGirl.create(:claimed_recording, is_public: true) + + recordings = Recording.popular_recordings + recordings.length.should eq(0) + end + end + describe "cleanup_excessive_storage" do sample_audio='sample.file' diff --git a/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb b/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb index 3f69ce5eb..ba89aee20 100644 --- a/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb +++ b/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb @@ -9,6 +9,7 @@ require 'spec_helper' describe RecurlyTransactionWebHook do + let(:refund_xml) {' @@ -120,8 +121,15 @@ describe RecurlyTransactionWebHook do it "deletes jam_track_right when refunded" do + sale = Sale.create_jam_track_sale(@user) + sale.recurly_invoice_id = '2da71ad9c657adf9fe618e4f058c78bb' + sale.recurly_total_in_cents = 216 + sale.save! # create a jam_track right, which should be whacked as soon as we craete the web hook - jam_track_right = FactoryGirl.create(:jam_track_right, user: @user, recurly_subscription_uuid: '2da71ad97c826a7b784c264ac59c04de') + jam_track_right = FactoryGirl.create(:jam_track_right, user: @user, recurly_adjustment_uuid: 'bleh') + + shopping_cart = ShoppingCart.create(@user, jam_track_right.jam_track) + SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, '2da71ad9c657adf9fe618e4f058c78bb', nil) document = Nokogiri::XML(refund_xml) @@ -131,8 +139,16 @@ describe RecurlyTransactionWebHook do end it "deletes jam_track_right when voided" do + + sale = Sale.create_jam_track_sale(@user) + sale.recurly_invoice_id = '2da71ad9c657adf9fe618e4f058c78bb' + sale.recurly_total_in_cents = 216 + sale.save! # create a jam_track right, which should be whacked as soon as we craete the web hook - jam_track_right = FactoryGirl.create(:jam_track_right, user: @user, recurly_subscription_uuid: '2da71ad97c826a7b784c264ac59c04de') + jam_track_right = FactoryGirl.create(:jam_track_right, user: @user, recurly_adjustment_uuid: 'blah') + + shopping_cart = ShoppingCart.create(@user, jam_track_right.jam_track) + SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, '2da71ad9c657adf9fe618e4f058c78bb', nil) document = Nokogiri::XML(void_xml) diff --git a/ruby/spec/jam_ruby/models/rsvp_request_spec.rb b/ruby/spec/jam_ruby/models/rsvp_request_spec.rb index 0f8aa5874..f93501647 100644 --- a/ruby/spec/jam_ruby/models/rsvp_request_spec.rb +++ b/ruby/spec/jam_ruby/models/rsvp_request_spec.rb @@ -104,7 +104,7 @@ describe RsvpRequest do it "should not allow non-invitee to RSVP to session with closed RSVPs" do @music_session.open_rsvps = false @music_session.save! - expect {RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id]}, @non_session_invitee)}.to raise_error(JamRuby::PermissionError) + expect {RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id]}, @non_session_invitee)}.to raise_error(JamRuby::JamPermissionError) end it "should allow RSVP creation with autoapprove option for open RSVP sessions" do @@ -216,7 +216,7 @@ describe RsvpRequest do # attempt to approve with non-organizer rs1 = RsvpRequestRsvpSlot.find_by_rsvp_slot_id(@slot1.id) rs2 = RsvpRequestRsvpSlot.find_by_rsvp_slot_id(@slot2.id) - expect {RsvpRequest.update({:id => rsvp.id, :session_id => @music_session.id, :rsvp_responses => [{:request_slot_id => rs1.id, :accept => true}, {:request_slot_id => rs2.id, :accept => true}]}, @session_invitee)}.to raise_error(PermissionError) + expect {RsvpRequest.update({:id => rsvp.id, :session_id => @music_session.id, :rsvp_responses => [{:request_slot_id => rs1.id, :accept => true}, {:request_slot_id => rs2.id, :accept => true}]}, @session_invitee)}.to raise_error(JamPermissionError) # approve with organizer rs1 = RsvpRequestRsvpSlot.find_by_rsvp_slot_id(@slot1.id) @@ -402,7 +402,7 @@ describe RsvpRequest do comment.comment.should == "Let's Jam!" # cancel - expect {RsvpRequest.cancel({:id => rsvp.id, :session_id => @music_session.id, :cancelled => "all", :message => "I'm gonna cancel all your RSVPs"}, user)}.to raise_error(PermissionError) + expect {RsvpRequest.cancel({:id => rsvp.id, :session_id => @music_session.id, :cancelled => "all", :message => "I'm gonna cancel all your RSVPs"}, user)}.to raise_error(JamPermissionError) end end diff --git a/ruby/spec/jam_ruby/models/sale_line_item_spec.rb b/ruby/spec/jam_ruby/models/sale_line_item_spec.rb new file mode 100644 index 000000000..334166734 --- /dev/null +++ b/ruby/spec/jam_ruby/models/sale_line_item_spec.rb @@ -0,0 +1,41 @@ + +require 'spec_helper' + +describe SaleLineItem do + + let(:user) {FactoryGirl.create(:user)} + let(:user2) {FactoryGirl.create(:user)} + let(:jam_track) {FactoryGirl.create(:jam_track)} + + describe "associations" do + + it "can find associated recurly transaction web hook" do + sale = Sale.create_jam_track_sale(user) + shopping_cart = ShoppingCart.create(user, jam_track) + sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, 'some_recurly_uuid', nil, nil) + transaction = FactoryGirl.create(:recurly_transaction_web_hook, subscription_id: 'some_recurly_uuid') + + sale_line_item.reload + sale_line_item.recurly_transactions.should eq([transaction]) + end + end + + + describe "state" do + + it "success" do + sale = Sale.create_jam_track_sale(user) + shopping_cart = ShoppingCart.create(user, jam_track) + sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, 'some_recurly_uuid', nil, nil) + transaction = FactoryGirl.create(:recurly_transaction_web_hook, subscription_id: 'some_recurly_uuid') + + sale_line_item.reload + sale_line_item.state.should eq({ + void: false, + refund: false, + fail: false, + success: true + }) + end + end +end diff --git a/ruby/spec/jam_ruby/models/sale_spec.rb b/ruby/spec/jam_ruby/models/sale_spec.rb index 0c9b089b1..a0d4acf48 100644 --- a/ruby/spec/jam_ruby/models/sale_spec.rb +++ b/ruby/spec/jam_ruby/models/sale_spec.rb @@ -1,72 +1,451 @@ - require 'spec_helper' describe Sale do - describe "check_integrity" do + let(:user) {FactoryGirl.create(:user)} + let(:user2) {FactoryGirl.create(:user)} + let(:jam_track) {FactoryGirl.create(:jam_track)} + + describe "index" do + it "empty" do + result = Sale.index(user) + result[:query].length.should eq(0) + result[:next].should eq(nil) + end + + it "one" do + sale = Sale.create_jam_track_sale(user) + shopping_cart = ShoppingCart.create(user, jam_track) + sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_adjustment_uuid', nil) + + result = Sale.index(user) + result[:query].length.should eq(1) + result[:next].should eq(nil) + end + + it "user filtered correctly" do + sale = Sale.create_jam_track_sale(user) + shopping_cart = ShoppingCart.create(user, jam_track) + sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_adjustment_uuid', nil) + + result = Sale.index(user) + result[:query].length.should eq(1) + result[:next].should eq(nil) + + sale2 = Sale.create_jam_track_sale(user2) + shopping_cart = ShoppingCart.create(user2, jam_track) + sale_line_item2 = SaleLineItem.create_from_shopping_cart(sale2, shopping_cart, nil, 'some_adjustment_uuid', nil) + + result = Sale.index(user) + result[:query].length.should eq(1) + result[:next].should eq(nil) + end + end + + + describe "place_order" do let(:user) {FactoryGirl.create(:user)} - let(:jam_track) {FactoryGirl.create(:jam_track)} + let(:jamtrack) { FactoryGirl.create(:jam_track) } + let(:jam_track_price_in_cents) { (jamtrack.price * 100).to_i } + let(:client) { RecurlyClient.new } + let(:billing_info) { + info = {} + info[:first_name] = user.first_name + info[:last_name] = user.last_name + info[:address1] = 'Test Address 1' + info[:address2] = 'Test Address 2' + info[:city] = user.city + info[:state] = user.state + info[:country] = user.country + info[:zip] = '12345' + info[:number] = '4111-1111-1111-1111' + info[:month] = '08' + info[:year] = '2017' + info[:verification_value] = '111' + info + } + + after(:each) do + if user.recurly_code + account = Recurly::Account.find(user.recurly_code) + if account.present? + account.destroy + end + end + end + + + it "for a free jam track" do + shopping_cart = ShoppingCart.create user, jamtrack, 1, true + client.find_or_create_account(user, billing_info) + + sales = Sale.place_order(user, [shopping_cart]) + + user.reload + user.sales.length.should eq(1) + + sales.should eq(user.sales) + sale = sales[0] + sale.recurly_invoice_id.should be_nil + + sale.recurly_subtotal_in_cents.should eq(0) + sale.recurly_tax_in_cents.should eq(0) + sale.recurly_total_in_cents.should eq(0) + sale.recurly_currency.should eq('USD') + sale.order_total.should eq(0) + sale.sale_line_items.length.should == 1 + sale_line_item = sale.sale_line_items[0] + sale_line_item.recurly_tax_in_cents.should eq(0) + sale_line_item.recurly_total_in_cents.should eq(0) + sale_line_item.recurly_currency.should eq('USD') + sale_line_item.recurly_discount_in_cents.should eq(0) + sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE) + sale_line_item.unit_price.should eq(jamtrack.price) + sale_line_item.quantity.should eq(1) + sale_line_item.free.should eq(1) + sale_line_item.sales_tax.should be_nil + sale_line_item.shipping_handling.should eq(0) + sale_line_item.recurly_plan_code.should eq(jamtrack.plan_code) + sale_line_item.product_id.should eq(jamtrack.id) + sale_line_item.recurly_subscription_uuid.should be_nil + sale_line_item.recurly_adjustment_uuid.should be_nil + sale_line_item.recurly_adjustment_credit_uuid.should be_nil + sale_line_item.recurly_adjustment_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_uuid) + sale_line_item.recurly_adjustment_credit_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_credit_uuid) + + # verify subscription is in Recurly + recurly_account = client.get_account(user) + adjustments = recurly_account.adjustments + adjustments.should_not be_nil + adjustments.should have(0).items + + invoices = recurly_account.invoices + invoices.should have(0).items + + + # verify jam_track_rights data + user.jam_track_rights.should_not be_nil + user.jam_track_rights.should have(1).items + user.jam_track_rights.last.jam_track.id.should eq(jamtrack.id) + user.jam_track_rights.last.redeemed.should be_true + user.has_redeemable_jamtrack.should be_false + end + + it "for a free jam track with an affiliate association" do + partner = FactoryGirl.create(:affiliate_partner) + user.affiliate_referral = partner + user.save! + + shopping_cart = ShoppingCart.create user, jamtrack, 1, true + client.find_or_create_account(user, billing_info) + + sales = Sale.place_order(user, [shopping_cart]) + + user.reload + user.sales.length.should eq(1) + + sales.should eq(user.sales) + sale = sales[0] + sale.recurly_invoice_id.should be_nil + + sale.recurly_subtotal_in_cents.should eq(0) + sale.recurly_tax_in_cents.should eq(0) + sale.recurly_total_in_cents.should eq(0) + sale.recurly_currency.should eq('USD') + sale.order_total.should eq(0) + sale.sale_line_items.length.should == 1 + sale_line_item = sale.sale_line_items[0] + sale_line_item.recurly_tax_in_cents.should eq(0) + sale_line_item.recurly_total_in_cents.should eq(0) + sale_line_item.recurly_currency.should eq('USD') + sale_line_item.recurly_discount_in_cents.should eq(0) + sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE) + sale_line_item.unit_price.should eq(jamtrack.price) + sale_line_item.quantity.should eq(1) + sale_line_item.free.should eq(1) + sale_line_item.sales_tax.should be_nil + sale_line_item.shipping_handling.should eq(0) + sale_line_item.recurly_plan_code.should eq(jamtrack.plan_code) + sale_line_item.product_id.should eq(jamtrack.id) + sale_line_item.recurly_subscription_uuid.should be_nil + sale_line_item.recurly_adjustment_uuid.should be_nil + sale_line_item.recurly_adjustment_credit_uuid.should be_nil + sale_line_item.recurly_adjustment_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_uuid) + sale_line_item.recurly_adjustment_credit_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_credit_uuid) + sale_line_item.affiliate_referral.should eq(partner) + sale_line_item.affiliate_referral_fee_in_cents.should eq(0) + + # verify subscription is in Recurly + recurly_account = client.get_account(user) + adjustments = recurly_account.adjustments + adjustments.should_not be_nil + adjustments.should have(0).items + + invoices = recurly_account.invoices + invoices.should have(0).items + + + # verify jam_track_rights data + user.jam_track_rights.should_not be_nil + user.jam_track_rights.should have(1).items + user.jam_track_rights.last.jam_track.id.should eq(jamtrack.id) + user.jam_track_rights.last.redeemed.should be_true + user.has_redeemable_jamtrack.should be_false + end + + + it "for a normally priced jam track" do + user.has_redeemable_jamtrack = false + user.save! + shopping_cart = ShoppingCart.create user, jamtrack, 1, false + client.find_or_create_account(user, billing_info) + + sales = Sale.place_order(user, [shopping_cart]) + + user.reload + user.sales.length.should eq(1) + + sales.should eq(user.sales) + sale = sales[0] + sale.recurly_invoice_id.should_not be_nil + + sale.recurly_subtotal_in_cents.should eq(jam_track_price_in_cents) + sale.recurly_tax_in_cents.should eq(0) + sale.recurly_total_in_cents.should eq(jam_track_price_in_cents) + sale.recurly_currency.should eq('USD') + + sale.order_total.should eq(jamtrack.price) + sale.sale_line_items.length.should == 1 + sale_line_item = sale.sale_line_items[0] + # validate we are storing pricing info from recurly + sale_line_item.recurly_tax_in_cents.should eq(0) + sale_line_item.recurly_total_in_cents.should eq(jam_track_price_in_cents) + sale_line_item.recurly_currency.should eq('USD') + sale_line_item.recurly_discount_in_cents.should eq(0) + sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE) + sale_line_item.unit_price.should eq(jamtrack.price) + sale_line_item.quantity.should eq(1) + sale_line_item.free.should eq(0) + sale_line_item.sales_tax.should be_nil + sale_line_item.shipping_handling.should eq(0) + sale_line_item.recurly_plan_code.should eq(jamtrack.plan_code) + sale_line_item.product_id.should eq(jamtrack.id) + sale_line_item.recurly_subscription_uuid.should be_nil + sale_line_item.recurly_adjustment_uuid.should_not be_nil + sale_line_item.recurly_adjustment_credit_uuid.should be_nil + sale_line_item.recurly_adjustment_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_uuid) + + # verify subscription is in Recurly + recurly_account = client.get_account(user) + adjustments = recurly_account.adjustments + adjustments.should_not be_nil + adjustments.should have(1).items + purchase= adjustments[0] + purchase.unit_amount_in_cents.should eq((jamtrack.price * 100).to_i) + purchase.accounting_code.should eq(ShoppingCart::PURCHASE_NORMAL) + purchase.description.should eq("JamTrack: " + jamtrack.name) + purchase.state.should eq('invoiced') + purchase.uuid.should eq(sale_line_item.recurly_adjustment_uuid) + + invoices = recurly_account.invoices + invoices.should have(1).items + invoice = invoices[0] + invoice.uuid.should eq(sale.recurly_invoice_id) + invoice.line_items.should have(1).items # should have single adjustment associated + invoice.line_items[0].should eq(purchase) + invoice.subtotal_in_cents.should eq((jamtrack.price * 100).to_i) + invoice.total_in_cents.should eq((jamtrack.price * 100).to_i) + invoice.state.should eq('collected') + + # verify jam_track_rights data + user.jam_track_rights.should_not be_nil + user.jam_track_rights.should have(1).items + user.jam_track_rights.last.jam_track.id.should eq(jamtrack.id) + user.jam_track_rights.last.redeemed.should be_false + user.has_redeemable_jamtrack.should be_false + + sale_line_item.affiliate_referral.should be_nil + sale_line_item.affiliate_referral_fee_in_cents.should be_nil + end + + + it "for a normally priced jam track with an affiliate association" do + user.has_redeemable_jamtrack = false + partner = FactoryGirl.create(:affiliate_partner) + user.affiliate_referral = partner + user.save! + shopping_cart = ShoppingCart.create user, jamtrack, 1, false + client.find_or_create_account(user, billing_info) + + sales = Sale.place_order(user, [shopping_cart]) + + user.reload + user.sales.length.should eq(1) + + sales.should eq(user.sales) + sale = sales[0] + sale.recurly_invoice_id.should_not be_nil + + sale.recurly_subtotal_in_cents.should eq(jam_track_price_in_cents) + sale.recurly_tax_in_cents.should eq(0) + sale.recurly_total_in_cents.should eq(jam_track_price_in_cents) + sale.recurly_currency.should eq('USD') + + sale.order_total.should eq(jamtrack.price) + sale.sale_line_items.length.should == 1 + sale_line_item = sale.sale_line_items[0] + # validate we are storing pricing info from recurly + sale_line_item.recurly_tax_in_cents.should eq(0) + sale_line_item.recurly_total_in_cents.should eq(jam_track_price_in_cents) + sale_line_item.recurly_currency.should eq('USD') + sale_line_item.recurly_discount_in_cents.should eq(0) + sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE) + sale_line_item.unit_price.should eq(jamtrack.price) + sale_line_item.quantity.should eq(1) + sale_line_item.free.should eq(0) + sale_line_item.sales_tax.should be_nil + sale_line_item.shipping_handling.should eq(0) + sale_line_item.recurly_plan_code.should eq(jamtrack.plan_code) + sale_line_item.product_id.should eq(jamtrack.id) + sale_line_item.recurly_subscription_uuid.should be_nil + sale_line_item.recurly_adjustment_uuid.should_not be_nil + sale_line_item.recurly_adjustment_credit_uuid.should be_nil + sale_line_item.recurly_adjustment_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_uuid) + sale_line_item.affiliate_referral.should eq(partner) + sale_line_item.affiliate_referral_fee_in_cents.should eq(20) + + # verify subscription is in Recurly + recurly_account = client.get_account(user) + adjustments = recurly_account.adjustments + adjustments.should_not be_nil + adjustments.should have(1).items + purchase= adjustments[0] + purchase.unit_amount_in_cents.should eq((jamtrack.price * 100).to_i) + purchase.accounting_code.should eq(ShoppingCart::PURCHASE_NORMAL) + purchase.description.should eq("JamTrack: " + jamtrack.name) + purchase.state.should eq('invoiced') + purchase.uuid.should eq(sale_line_item.recurly_adjustment_uuid) + + invoices = recurly_account.invoices + invoices.should have(1).items + invoice = invoices[0] + invoice.uuid.should eq(sale.recurly_invoice_id) + invoice.line_items.should have(1).items # should have single adjustment associated + invoice.line_items[0].should eq(purchase) + invoice.subtotal_in_cents.should eq((jamtrack.price * 100).to_i) + invoice.total_in_cents.should eq((jamtrack.price * 100).to_i) + invoice.state.should eq('collected') + + # verify jam_track_rights data + user.jam_track_rights.should_not be_nil + user.jam_track_rights.should have(1).items + user.jam_track_rights.last.jam_track.id.should eq(jamtrack.id) + user.jam_track_rights.last.redeemed.should be_false + user.has_redeemable_jamtrack.should be_false + end + + it "for a jamtrack already owned" do + shopping_cart = ShoppingCart.create user, jamtrack, 1, true + client.find_or_create_account(user, billing_info) + + sales = Sale.place_order(user, [shopping_cart]) + + user.reload + user.sales.length.should eq(1) + + shopping_cart = ShoppingCart.create user, jamtrack, 1, false + sales = Sale.place_order(user, [shopping_cart]) + sales.should have(0).items + # also, verify that no earlier adjustments were affected + recurly_account = client.get_account(user) + adjustments = recurly_account.adjustments + adjustments.should have(0).items # because the only successful purchase was a freebie, there should be no recurly adjustments + end + + # this test counts on the fact that two adjustments are made when buying a free JamTrack + # so if we make the second adjustment invalid from Recurly's standpoint, then + # we can see if the first one is ultimately destroyed + it "rolls back created adjustments if error" do + + shopping_cart = ShoppingCart.create user, jamtrack, 1, false + + # grab the real response; we will modify it to make a nil accounting code + adjustment_attrs = shopping_cart.create_adjustment_attributes(user) + client.find_or_create_account(user, billing_info) + + adjustment_attrs[0][:unit_amount_in_cents] = nil # invalid amount + ShoppingCart.any_instance.stub(:create_adjustment_attributes).and_return(adjustment_attrs) + + expect { Sale.place_order(user, [shopping_cart]) }.to raise_error(JamRuby::RecurlyClientError) + + user.reload + user.sales.should have(0).items + + recurly_account = client.get_account(user) + recurly_account.adjustments.should have(0).items + end + + it "rolls back adjustments created before the order" do + shopping_cart = ShoppingCart.create user, jamtrack, 1, false + client.find_or_create_account(user, billing_info) + + # create a single adjustment on the account + adjustment_attrs = shopping_cart.create_adjustment_attributes(user) + recurly_account = client.get_account(user) + adjustment = recurly_account.adjustments.new (adjustment_attrs[0]) + adjustment.save + adjustment.errors.any?.should be_false + + sales = Sale.place_order(user, [shopping_cart]) + + user.reload + + recurly_account = client.get_account(user) + adjustments = recurly_account.adjustments + adjustments.should have(1).items # two adjustments are created for a free jamtrack; that should be all there is + end + end + + describe "check_integrity_of_jam_track_sales" do + + let(:user) { FactoryGirl.create(:user) } + let(:jam_track) { FactoryGirl.create(:jam_track) } it "empty" do - check_integrity = Sale.check_integrity + check_integrity = Sale.check_integrity_of_jam_track_sales check_integrity.length.should eq(1) r = check_integrity[0] r.total.to_i.should eq(0) - r.not_known.to_i.should eq(0) - r.succeeded.to_i.should eq(0) - r.failed.to_i.should eq(0) - r.refunded.to_i.should eq(0) - r.voided.to_i.should eq(0) - end - - it "one unknown sale" do - sale = Sale.create(user) - shopping_cart = ShoppingCart.create(user, jam_track) - SaleLineItem.create_from_shopping_cart(sale, shopping_cart, 'some_recurly_uuid') - - check_integrity = Sale.check_integrity - r = check_integrity[0] - r.total.to_i.should eq(1) - r.not_known.to_i.should eq(1) - r.succeeded.to_i.should eq(0) - r.failed.to_i.should eq(0) - r.refunded.to_i.should eq(0) r.voided.to_i.should eq(0) end it "one succeeded sale" do - sale = Sale.create(user) + sale = Sale.create_jam_track_sale(user) shopping_cart = ShoppingCart.create(user, jam_track) - SaleLineItem.create_from_shopping_cart(sale, shopping_cart, 'some_recurly_uuid') - FactoryGirl.create(:recurly_transaction_web_hook, subscription_id: 'some_recurly_uuid') + SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_recurly_invoice_id', nil) - - check_integrity = Sale.check_integrity + check_integrity = Sale.check_integrity_of_jam_track_sales r = check_integrity[0] r.total.to_i.should eq(1) - r.not_known.to_i.should eq(0) - r.succeeded.to_i.should eq(1) - r.failed.to_i.should eq(0) - r.refunded.to_i.should eq(0) r.voided.to_i.should eq(0) end - it "one failed sale" do - sale = Sale.create(user) - shopping_cart = ShoppingCart.create(user, jam_track) - SaleLineItem.create_from_shopping_cart(sale, shopping_cart, 'some_recurly_uuid') - FactoryGirl.create(:recurly_transaction_web_hook_failed, subscription_id: 'some_recurly_uuid') - check_integrity = Sale.check_integrity + it "one voided sale" do + sale = Sale.create_jam_track_sale(user) + sale.recurly_invoice_id = 'some_recurly_invoice_id' + sale.save! + shopping_cart = ShoppingCart.create(user, jam_track) + SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_recurly_invoice_id', nil) + FactoryGirl.create(:recurly_transaction_web_hook, transaction_type: RecurlyTransactionWebHook::VOID, invoice_id: 'some_recurly_invoice_id') + + check_integrity = Sale.check_integrity_of_jam_track_sales r = check_integrity[0] r.total.to_i.should eq(1) - r.not_known.to_i.should eq(0) - r.succeeded.to_i.should eq(0) - r.failed.to_i.should eq(1) - r.refunded.to_i.should eq(0) - r.voided.to_i.should eq(0) + r.voided.to_i.should eq(1) 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 index 9daf3d449..6be02e8d4 100644 --- a/ruby/spec/jam_ruby/models/shopping_cart_spec.rb +++ b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb @@ -21,33 +21,36 @@ describe ShoppingCart do user.shopping_carts[0].quantity.should == 1 end - it "should not add duplicate JamTrack to ShoppingCart" do + + it "maintains only one fre JamTrack in ShoppingCart" do cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track) cart1.should_not be_nil + cart1.errors.any?.should be_false user.reload cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track) - cart2.should be_nil + cart2.errors.any?.should be_false + user.reload + user.shopping_carts.length.should eq(1) + cart3 = ShoppingCart.add_jam_track_to_cart(user, jam_track2) + cart3.errors.any?.should be_false + user.reload + user.shopping_carts.length.should eq(1) + end + it "should not add duplicate JamTrack to ShoppingCart" do + user.has_redeemable_jamtrack = false + user.save! + cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track) + cart1.should_not be_nil + cart1.errors.any?.should be_false + user.reload + cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track) + cart2.errors.any?.should be_true end describe "redeemable behavior" do - it "adds redeemable item to shopping cart" do - user.has_redeemable_jamtrack.should be_true - - # first item added to shopping cart should be marked for redemption - cart = ShoppingCart.add_jam_track_to_cart(user, jam_track) - cart.marked_for_redeem.should eq(1) - - # but the second item should not - - user.reload - - cart = ShoppingCart.add_jam_track_to_cart(user, jam_track2) - cart.marked_for_redeem.should eq(0) - end - - it "removes redeemable item to shopping cart" do + it "removes redeemable item to shopping cart (maintains only one in cart)" do user.has_redeemable_jamtrack.should be_true cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track) @@ -57,12 +60,12 @@ describe ShoppingCart do cart2.should_not be_nil cart1.marked_for_redeem.should eq(1) - cart2.marked_for_redeem.should eq(0) + cart2.marked_for_redeem.should eq(1) ShoppingCart.remove_jam_track_from_cart(user, jam_track) - user.shopping_carts.length.should eq(1) + user.shopping_carts.length.should eq(0) cart2.reload - cart1.marked_for_redeem.should eq(1) + cart2.marked_for_redeem.should eq(1) end end end diff --git a/ruby/spec/jam_ruby/models/signup_hint_spec.rb b/ruby/spec/jam_ruby/models/signup_hint_spec.rb new file mode 100644 index 000000000..fc17d2e03 --- /dev/null +++ b/ruby/spec/jam_ruby/models/signup_hint_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe SignupHint do + + let(:user) {AnonymousUser.new(SecureRandom.uuid, nil)} + + describe "refresh_by_anoymous_user" do + it "creates" do + hint = SignupHint.refresh_by_anoymous_user(user, {redirect_location: 'abc'}) + hint.errors.any?.should be_false + hint.redirect_location.should eq('abc') + hint.want_jamblaster.should be_false + end + + it "updated" do + SignupHint.refresh_by_anoymous_user(user, {redirect_location: 'abc'}) + + hint = SignupHint.refresh_by_anoymous_user(user, {redirect_location: nil, want_jamblaster: true}) + hint.errors.any?.should be_false + hint.redirect_location.should be_nil + hint.want_jamblaster.should be_true + end + end +end diff --git a/ruby/spec/jam_ruby/recurly_client_spec.rb b/ruby/spec/jam_ruby/recurly_client_spec.rb index cf51c4b8c..108fe1600 100644 --- a/ruby/spec/jam_ruby/recurly_client_spec.rb +++ b/ruby/spec/jam_ruby/recurly_client_spec.rb @@ -86,42 +86,7 @@ describe RecurlyClient do found.state.should eq('closed') end - it "can place order" do - sale = Sale.create(@user) - sale = Sale.find(sale.id) - shopping_cart = ShoppingCart.create @user, @jamtrack, 1, true - history_items = @client.payment_history(@user).length - @client.find_or_create_account(@user, @billing_info) - expect{@client.place_order(@user, @jamtrack, shopping_cart, sale)}.not_to raise_error() - - # verify jam_track_rights data - @user.jam_track_rights.should_not be_nil - @user.jam_track_rights.should have(1).items - @user.jam_track_rights.last.jam_track.id.should eq(@jamtrack.id) - - # verify sales data - sale = Sale.find(sale.id) - sale.sale_line_items.length.should == 1 - sale_line_item = sale.sale_line_items[0] - sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE) - sale_line_item.unit_price.should eq(@jamtrack.price) - sale_line_item.quantity.should eq(1) - sale_line_item.free.should eq(1) - sale_line_item.sales_tax.should be_nil - sale_line_item.shipping_handling.should eq(0) - sale_line_item.recurly_plan_code.should eq(@jamtrack.plan_code) - sale_line_item.product_id.should eq(@jamtrack.id) - sale_line_item.recurly_subscription_uuid.should_not be_nil - sale_line_item.recurly_subscription_uuid.should eq(@user.jam_track_rights.last.recurly_subscription_uuid) - - # verify subscription is in Recurly - subs = @client.get_account(@user).subscriptions - subs.should_not be_nil - subs.should have(1).items - - @client.payment_history(@user).should have(history_items+1).items - end - +=begin it "can refund subscription" do sale = Sale.create(@user) shopping_cart = ShoppingCart.create @user, @jamtrack, 1 @@ -141,18 +106,7 @@ describe RecurlyClient do @jamtrack.reload @jamtrack.jam_track_rights.should have(0).items end +=end - it "detects error on double order" do - sale = Sale.create(@user) - shopping_cart = ShoppingCart.create @user, @jamtrack, 1 - @client.find_or_create_account(@user, @billing_info) - jam_track_right = @client.place_order(@user, @jamtrack, shopping_cart, sale) - jam_track_right.recurly_subscription_uuid.should_not be_nil - - shopping_cart = ShoppingCart.create @user, @jamtrack, 1 - jam_track_right2 = @client.place_order(@user, @jamtrack, shopping_cart, sale) - jam_track_right.should eq(jam_track_right2) - jam_track_right.recurly_subscription_uuid.should eq(jam_track_right.recurly_subscription_uuid) - end end diff --git a/ruby/spec/jam_ruby/resque/jam_tracks_builder_spec.rb b/ruby/spec/jam_ruby/resque/jam_tracks_builder_spec.rb index 7ead35e3a..982c56f87 100644 --- a/ruby/spec/jam_ruby/resque/jam_tracks_builder_spec.rb +++ b/ruby/spec/jam_ruby/resque/jam_tracks_builder_spec.rb @@ -44,8 +44,13 @@ describe JamTracksBuilder do jam_track_right[:url_44].should be_nil JamTracksBuilder.perform(jam_track_right.id, 48) jam_track_right.reload - jam_track_right[:url_48].should == jam_track_right.store_dir + '/' + jam_track_right.filename + jam_track_right[:url_48].should == jam_track_right.store_dir + '/' + jam_track_right.filename(:url_48) jam_track_track[:url_44].should be_nil + + jam_track_right.signed_48.should be_true + jam_track_right.signing_started_at_48.should_not be_nil + jam_track_right.signed_44.should be_false + jam_track_right.signing_started_at_44.should be_nil end describe "with bitrate 44" do @@ -76,9 +81,14 @@ describe JamTracksBuilder do jam_track_right[:url_48].should be_nil JamTracksBuilder.perform(jam_track_right.id, 44) jam_track_right.reload - jam_track_right[:url_44].should == jam_track_right.store_dir + '/' + jam_track_right.filename + jam_track_right[:url_44].should == jam_track_right.store_dir + '/' + jam_track_right.filename(:url_44) jam_track_right.url_44.should_not be_nil jam_track_track[:url_48].should be_nil + + jam_track_right.signed_44.should be_true + jam_track_right.signing_started_at_44.should_not be_nil + jam_track_right.signed_48.should be_false + jam_track_right.signing_started_at_48.should be_nil end end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/resque/jam_tracks_cleaner_spec.rb b/ruby/spec/jam_ruby/resque/jam_tracks_cleaner_spec.rb index 1c900bc52..9f683c3e2 100644 --- a/ruby/spec/jam_ruby/resque/jam_tracks_cleaner_spec.rb +++ b/ruby/spec/jam_ruby/resque/jam_tracks_cleaner_spec.rb @@ -22,15 +22,16 @@ describe JamTracksCleaner do end it "should clean" do + pending "re-enable cleaner after manual testing" jam_track_right = JamTrackRight.create(:user=>@user, :jam_track=>@jam_track) - jam_track_right.signed=true + jam_track_right.signed_48=true jam_track_right jam_track_right.url_48.store!(File.open(RIGHT_NAME)) jam_track_right.downloaded_since_sign=true jam_track_right.save! - jam_track_right[:url_48].should == jam_track_right.store_dir + '/' + jam_track_right.filename + jam_track_right[:url_48].should == jam_track_right.store_dir + '/' + jam_track_right.filename(:url_48) jam_track_right.reload # Should exist after uploading: @@ -48,6 +49,6 @@ describe JamTracksCleaner do # But not after running cleaner job: JamRuby::JamTracksCleaner.perform - s3.exists?(url).should be_false + s3.exists?(url).should be_false end end \ No newline at end of file diff --git a/ruby/spec/mailers/user_mailer_spec.rb b/ruby/spec/mailers/user_mailer_spec.rb index 62b1472c6..c3d041060 100644 --- a/ruby/spec/mailers/user_mailer_spec.rb +++ b/ruby/spec/mailers/user_mailer_spec.rb @@ -12,6 +12,7 @@ describe UserMailer do let(:user) { FactoryGirl.create(:user) } before(:each) do + stub_const("APP_CONFIG", app_config) UserMailer.deliveries.clear end diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 94294b86c..8d16dffbb 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -3,6 +3,22 @@ JAMKAZAM_TESTING_BUCKET = 'jamkazam-testing' # cuz i'm not comfortable using aws def app_config klass = Class.new do + def admin_root_url + 'http://localhost:3333' + end + + def email_alerts_alias + 'alerts@jamkazam.com' + end + + def email_recurly_notice + 'recurly-alerts@jamkazam.com' + end + + def email_generic_from + 'nobody@jamkazam.com' + end + def aws_bucket JAMKAZAM_TESTING_BUCKET end @@ -158,8 +174,8 @@ def app_config false end - def signing_job_run_max_time - 60 # 1 minute + def signing_step_max_time + 40 # 40 seconds end def signing_job_queue_max_time @@ -170,6 +186,25 @@ def app_config true end + def secret_token + 'foobar' + end + + def unsubscribe_token + 'blah' + end + + def error_on_fraud + true + end + + def expire_fingerprint_days + 14 + end + + def found_conflict_count + 1 + end private @@ -240,4 +275,4 @@ end def friend(user1, user2) FactoryGirl.create(:friendship, user: user1, friend: user2) FactoryGirl.create(:friendship, user: user2, friend: user1) -end \ No newline at end of file +end diff --git a/web/.gitignore b/web/.gitignore index 48289754a..e305498b8 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -23,6 +23,7 @@ doc/ .idea *.iml +jt_metadata.json artifacts diff --git a/web/Gemfile b/web/Gemfile index c7cb8debf..a141bc9ef 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -18,6 +18,7 @@ else gem 'jam_websockets', "0.1.#{ENV["BUILD_NUMBER"]}" ENV['NOKOGIRI_USE_SYSTEM_LIBRARIES'] ||= "true" end + gem 'oj', '2.10.2' gem 'builder' gem 'rails', '~>3.2.11' @@ -87,6 +88,14 @@ gem 'recurly' gem 'guard', '2.7.3' gem 'influxdb', '0.1.8' gem 'influxdb-rails', '0.1.10' +gem 'sitemap_generator' +gem 'bower-rails', "~> 0.9.2" +gem 'react-rails', '~> 1.0' +gem "browserify-rails", "~> 0.7" + +source 'https://rails-assets.org' do + gem 'rails-assets-fluxxor' +end group :development, :test do gem 'rspec-rails', '2.14.2' @@ -97,7 +106,8 @@ group :development, :test do gem 'execjs', '1.4.0' gem 'factory_girl_rails', '4.1.0' # in dev because in use by rake task gem 'database_cleaner', '1.3.0' #in dev because in use by rake task - gem 'teaspoon' +# gem 'teaspoon' +# gem 'teaspoon-jasmine' end group :unix do gem 'therubyracer' #, '0.11.0beta8' @@ -132,7 +142,7 @@ group :test, :cucumber do # gem 'growl', '1.0.3' gem 'poltergeist' gem 'resque_spec' - #gem 'thin' + #gem 'thin' end diff --git a/web/README.md b/web/README.md index 975118c93..393628cf1 100644 --- a/web/README.md +++ b/web/README.md @@ -3,3 +3,4 @@ Jasmine Javascript Unit Tests Open browser to localhost:3000/teaspoon + diff --git a/web/Rakefile b/web/Rakefile index f4019acfc..882005f42 100644 --- a/web/Rakefile +++ b/web/Rakefile @@ -6,6 +6,7 @@ #require 'resque/scheduler/tasks' require 'resque/tasks' require 'resque/scheduler/tasks' +require 'sitemap_generator/tasks' require File.expand_path('../config/application', __FILE__) SampleApp::Application.load_tasks diff --git a/web/app/assets/fonts/Raleway/Raleway-Bold.ttf b/web/app/assets/fonts/Raleway/Raleway-Bold.ttf new file mode 100644 index 000000000..adc44af0b Binary files /dev/null and b/web/app/assets/fonts/Raleway/Raleway-Bold.ttf differ diff --git a/web/app/assets/fonts/Raleway/Raleway-Bold.woff b/web/app/assets/fonts/Raleway/Raleway-Bold.woff new file mode 100644 index 000000000..f4e18af9e Binary files /dev/null and b/web/app/assets/fonts/Raleway/Raleway-Bold.woff differ diff --git a/web/app/assets/fonts/Raleway/Raleway-Bold.woff2 b/web/app/assets/fonts/Raleway/Raleway-Bold.woff2 new file mode 100644 index 000000000..a7f145791 Binary files /dev/null and b/web/app/assets/fonts/Raleway/Raleway-Bold.woff2 differ diff --git a/web/app/assets/fonts/Raleway/Raleway-Heavy.ttf b/web/app/assets/fonts/Raleway/Raleway-Heavy.ttf new file mode 100644 index 000000000..b32bcf64c Binary files /dev/null and b/web/app/assets/fonts/Raleway/Raleway-Heavy.ttf differ diff --git a/web/app/assets/fonts/Raleway/Raleway-Heavy.woff b/web/app/assets/fonts/Raleway/Raleway-Heavy.woff new file mode 100644 index 000000000..9177ea802 Binary files /dev/null and b/web/app/assets/fonts/Raleway/Raleway-Heavy.woff differ diff --git a/web/app/assets/fonts/Raleway/Raleway-Heavy.woff2 b/web/app/assets/fonts/Raleway/Raleway-Heavy.woff2 new file mode 100644 index 000000000..6f1e37f42 Binary files /dev/null and b/web/app/assets/fonts/Raleway/Raleway-Heavy.woff2 differ diff --git a/web/app/assets/fonts/Raleway/Raleway-Light.ttf b/web/app/assets/fonts/Raleway/Raleway-Light.ttf new file mode 100644 index 000000000..5362e52fe Binary files /dev/null and b/web/app/assets/fonts/Raleway/Raleway-Light.ttf differ diff --git a/web/app/assets/fonts/Raleway/Raleway-Light.woff b/web/app/assets/fonts/Raleway/Raleway-Light.woff new file mode 100644 index 000000000..85f92d954 Binary files /dev/null and b/web/app/assets/fonts/Raleway/Raleway-Light.woff differ diff --git a/web/app/assets/fonts/Raleway/Raleway-Light.woff2 b/web/app/assets/fonts/Raleway/Raleway-Light.woff2 new file mode 100644 index 000000000..8e8ac1f40 Binary files /dev/null and b/web/app/assets/fonts/Raleway/Raleway-Light.woff2 differ diff --git a/web/app/assets/fonts/Raleway/Raleway-Thin.ttf b/web/app/assets/fonts/Raleway/Raleway-Thin.ttf new file mode 100644 index 000000000..3721c39af Binary files /dev/null and b/web/app/assets/fonts/Raleway/Raleway-Thin.ttf differ diff --git a/web/app/assets/fonts/Raleway/Raleway-Thin.woff b/web/app/assets/fonts/Raleway/Raleway-Thin.woff new file mode 100644 index 000000000..115b34776 Binary files /dev/null and b/web/app/assets/fonts/Raleway/Raleway-Thin.woff differ diff --git a/web/app/assets/fonts/Raleway/Raleway-Thin.woff2 b/web/app/assets/fonts/Raleway/Raleway-Thin.woff2 new file mode 100644 index 000000000..d3e82a0f6 Binary files /dev/null and b/web/app/assets/fonts/Raleway/Raleway-Thin.woff2 differ diff --git a/web/app/assets/fonts/Raleway/Raleway.eot b/web/app/assets/fonts/Raleway/Raleway.eot new file mode 100644 index 000000000..fc7ce0746 Binary files /dev/null and b/web/app/assets/fonts/Raleway/Raleway.eot differ diff --git a/web/app/assets/fonts/Raleway/Raleway.svg b/web/app/assets/fonts/Raleway/Raleway.svg new file mode 100644 index 000000000..92afeb5d6 --- /dev/null +++ b/web/app/assets/fonts/Raleway/Raleway.svg @@ -0,0 +1,2168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/app/assets/fonts/Raleway/Raleway.ttf b/web/app/assets/fonts/Raleway/Raleway.ttf new file mode 100644 index 000000000..3b354c87e Binary files /dev/null and b/web/app/assets/fonts/Raleway/Raleway.ttf differ diff --git a/web/app/assets/fonts/Raleway/Raleway.woff b/web/app/assets/fonts/Raleway/Raleway.woff new file mode 100644 index 000000000..02b0dd8f5 Binary files /dev/null and b/web/app/assets/fonts/Raleway/Raleway.woff differ diff --git a/web/app/assets/fonts/Raleway/Raleway.woff2 b/web/app/assets/fonts/Raleway/Raleway.woff2 new file mode 100644 index 000000000..35e3dbeee Binary files /dev/null and b/web/app/assets/fonts/Raleway/Raleway.woff2 differ diff --git a/web/app/assets/images/content/agree_button.png b/web/app/assets/images/content/agree_button.png new file mode 100644 index 000000000..b30ecf480 Binary files /dev/null and b/web/app/assets/images/content/agree_button.png differ diff --git a/web/app/assets/images/content/disagree_button.png b/web/app/assets/images/content/disagree_button.png new file mode 100644 index 000000000..9054b08fe Binary files /dev/null and b/web/app/assets/images/content/disagree_button.png differ diff --git a/web/app/assets/images/content/icon-pause-24-black.png b/web/app/assets/images/content/icon-pause-24-black.png new file mode 100644 index 000000000..2f719e15d Binary files /dev/null and b/web/app/assets/images/content/icon-pause-24-black.png differ diff --git a/web/app/assets/images/content/icon-pause-24-gray.png b/web/app/assets/images/content/icon-pause-24-gray.png new file mode 100644 index 000000000..38237b3ad Binary files /dev/null and b/web/app/assets/images/content/icon-pause-24-gray.png differ diff --git a/web/app/assets/images/content/icon-play-24-black.png b/web/app/assets/images/content/icon-play-24-black.png new file mode 100644 index 000000000..e21b8b5f9 Binary files /dev/null and b/web/app/assets/images/content/icon-play-24-black.png differ diff --git a/web/app/assets/images/content/icon-play-24-gray.png b/web/app/assets/images/content/icon-play-24-gray.png new file mode 100644 index 000000000..88c9c10ef Binary files /dev/null and b/web/app/assets/images/content/icon-play-24-gray.png differ diff --git a/web/app/assets/images/content/icon_cam.png b/web/app/assets/images/content/icon_cam.png new file mode 100644 index 000000000..e32bede67 Binary files /dev/null and b/web/app/assets/images/content/icon_cam.png differ diff --git a/web/app/assets/images/content/icon_stopbutton.png b/web/app/assets/images/content/icon_stopbutton.png new file mode 100644 index 000000000..37d19e686 Binary files /dev/null and b/web/app/assets/images/content/icon_stopbutton.png differ diff --git a/web/app/assets/images/content/icon_youtube_play.png b/web/app/assets/images/content/icon_youtube_play.png new file mode 100644 index 000000000..f947b2396 Binary files /dev/null and b/web/app/assets/images/content/icon_youtube_play.png differ diff --git a/web/app/assets/images/web/button_cta_jamblaster.png b/web/app/assets/images/web/button_cta_jamblaster.png new file mode 100644 index 000000000..8650fe322 Binary files /dev/null and b/web/app/assets/images/web/button_cta_jamblaster.png differ diff --git a/web/app/assets/images/web/button_cta_jamtrack.png b/web/app/assets/images/web/button_cta_jamtrack.png new file mode 100644 index 000000000..998d89b75 Binary files /dev/null and b/web/app/assets/images/web/button_cta_jamtrack.png differ diff --git a/web/app/assets/images/web/button_cta_platform.png b/web/app/assets/images/web/button_cta_platform.png new file mode 100644 index 000000000..dba8d499b Binary files /dev/null and b/web/app/assets/images/web/button_cta_platform.png differ diff --git a/web/app/assets/images/web/free-jamtrack-cta.png b/web/app/assets/images/web/free-jamtrack-cta.png new file mode 100644 index 000000000..c8f073167 Binary files /dev/null and b/web/app/assets/images/web/free-jamtrack-cta.png differ diff --git a/web/app/assets/images/web/generic_button_cta.png b/web/app/assets/images/web/generic_button_cta.png new file mode 100644 index 000000000..c0691f343 Binary files /dev/null and b/web/app/assets/images/web/generic_button_cta.png differ diff --git a/web/app/assets/images/web/thumbnail_buzz.jpg b/web/app/assets/images/web/thumbnail_buzz.jpg new file mode 100644 index 000000000..737a17873 Binary files /dev/null and b/web/app/assets/images/web/thumbnail_buzz.jpg differ diff --git a/web/app/assets/images/web/thumbnail_jamblaster.jpg b/web/app/assets/images/web/thumbnail_jamblaster.jpg new file mode 100644 index 000000000..7eef7d267 Binary files /dev/null and b/web/app/assets/images/web/thumbnail_jamblaster.jpg differ diff --git a/web/app/assets/images/web/thumbnail_jamtracks.jpg b/web/app/assets/images/web/thumbnail_jamtracks.jpg new file mode 100644 index 000000000..98bb27db5 Binary files /dev/null and b/web/app/assets/images/web/thumbnail_jamtracks.jpg differ diff --git a/web/app/assets/images/web/thumbnail_platform.jpg b/web/app/assets/images/web/thumbnail_platform.jpg new file mode 100644 index 000000000..8adacef59 Binary files /dev/null and b/web/app/assets/images/web/thumbnail_platform.jpg differ diff --git a/web/app/assets/javascripts/accounts.js b/web/app/assets/javascripts/accounts.js index 5df29a6bd..3d52d2139 100644 --- a/web/app/assets/javascripts/accounts.js +++ b/web/app/assets/javascripts/accounts.js @@ -8,6 +8,8 @@ var rest = context.JK.Rest(); var userId; var user = {}; + var screen = null; + var gearUtils = context.JK.GearUtilsInstance; function beforeShow(data) { userId = data.id; @@ -43,6 +45,19 @@ var invalidProfiles = prettyPrintAudioProfiles(context.JK.getBadConfigMap()); var sessionSummary = summarizeSession(userDetail); + if(gon.global.video_available && gon.global.video_available!="none" ) { + var webcamName; + var webcam = context.jamClient.FTUECurrentSelectedVideoDevice() + if (webcam == null || typeof(webcam) == "undefined" || Object.keys(webcam).length == 0) { + webcamName = "None Configured" + } else { + webcamName = _.values(webcam)[0] + } + } + else { + webcamName = 'video unavailable' + } + var $template = $(context._.template($('#template-account-main').html(), { email: userDetail.email, name: userDetail.name, @@ -55,7 +70,12 @@ validProfiles : validProfiles, invalidProfiles : invalidProfiles, isNativeClient: gon.isNativeClient, - musician: context.JK.currentUserMusician + musician: context.JK.currentUserMusician, + sales_count: userDetail.sales_count, + is_affiliate_partner: userDetail.is_affiliate_partner, + affiliate_earnings: (userDetail.affiliate_earnings / 100).toFixed(2), + affiliate_referral_count: userDetail.affiliate_referral_count, + webcamName: webcamName } , { variable: 'data' })); $('#account-content-scroller').html($template); @@ -64,15 +84,20 @@ $('#account-scheduled-sessions-link').show(); } else { $('#account-scheduled-sessions-link').hide(); - } - } + } + + }// function function prettyPrintAudioProfiles(profileMap) { var profiles = ""; var delimiter = ", "; if (profileMap && profileMap.length > 0) { $.each(profileMap, function(index, val) { - profiles += val.name + delimiter; + var inputName = val.name; + if (inputName == gearUtils.JAMKAZAM_VIRTUAL_INPUT) { + inputName = gearUtils.SYSTEM_DEFAULT_PLAYBACK_ONLY; + } + profiles += inputName + delimiter; }); return profiles.substring(0, profiles.length - delimiter.length); @@ -109,11 +134,11 @@ $('#account-content-scroller').on('click', '#account-edit-subscriptions-link', function(evt) { evt.stopPropagation(); navToEditSubscriptions(); return false; } ); $('#account-content-scroller').on('click', '#account-edit-payments-link', function(evt) { evt.stopPropagation(); navToEditPayments(); return false; } ); $('#account-content-scroller').on('click', '#account-edit-audio-link', function(evt) { evt.stopPropagation(); navToEditAudio(); return false; } ); + $('#account-content-scroller').on('click', '#account-edit-video-link', function(evt) { evt.stopPropagation(); navToEditVideo(); return false; } ); $('#account-content-scroller').on('avatar_changed', '#profile-avatar', function(evt, newAvatarUrl) { evt.stopPropagation(); updateAvatar(newAvatarUrl); return false; }) - // License dialog: - $("#account-content-scroller").on('click', '#account-view-license-link', function(evt) {evt.stopPropagation(); app.layout.showDialog('jamtrack-license-dialog'); return false; } ); - $("#account-content-scroller").on('click', '#account-payment-history-link', function(evt) {evt.stopPropagation(); app.layout.showDialog('jamtrack-payment-history-dialog'); return false; } ); + $("#account-content-scroller").on('click', '#account-payment-history-link', function(evt) {evt.stopPropagation(); navToPaymentHistory(); return false; } ); + $("#account-content-scroller").on('click', '#account-affiliate-partner-link', function(evt) {evt.stopPropagation(); navToAffiliates(); return false; } ); } function renderAccount() { @@ -157,6 +182,20 @@ window.location = "/client#/account/audio" } + function navToEditVideo() { + resetForm() + window.location = "/client#/account/video" + } + + function navToPaymentHistory() { + window.location = '/client#/account/paymentHistory' + } + + function navToAffiliates() { + resetForm() + window.location = '/client#/account/affiliatePartner' + } + // handle update avatar event function updateAvatar(avatar_url) { var photoUrl = context.JK.resolveAvatarUrl(avatar_url); @@ -173,6 +212,7 @@ } function initialize() { + screen = $('#account-content-scroller'); var screenBindings = { 'beforeShow': beforeShow, 'afterShow': afterShow @@ -187,4 +227,4 @@ return this; }; -})(window,jQuery); \ No newline at end of file +})(window,jQuery); diff --git a/web/app/assets/javascripts/accounts_affiliate.js b/web/app/assets/javascripts/accounts_affiliate.js new file mode 100644 index 000000000..21d0b8b1c --- /dev/null +++ b/web/app/assets/javascripts/accounts_affiliate.js @@ -0,0 +1,324 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.AccountAffiliateScreen = function (app) { + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var userId; + var user = {}; + var affiliatePartnerTabs = ['account', 'agreement', 'signups', 'earnings']; + var affiliatePartnerData = null; + var $screen = null; + + function beforeShow(data) { + userId = data.id; + affiliatePartnerData = null; + } + + function afterShow(data) { + + rest.getAffiliatePartnerData(userId) + .done(function (response) { + affiliatePartnerData = response; + renderAffiliateTab('account') + }) + .fail(app.ajaxError) + + } + + function events() { + + // Affiliate Partner + $("#account-affiliate-partner-content-scroller").on('click', '#affiliate-partner-account-link', function (evt) { + evt.stopPropagation(); + renderAffiliateTab('account'); + return false; + }); + $("#account-affiliate-partner-content-scroller").on('click', '#affiliate-partner-links-link', function (evt) { + evt.stopPropagation(); + renderAffiliateTab('links'); + return false; + }); + $("#account-affiliate-partner-content-scroller").on('click', '#affiliate-partner-agreement-link', function (evt) { + evt.stopPropagation(); + renderAffiliateTab('agreement'); + return false; + }); + $("#account-affiliate-partner-content-scroller").on('click', '#affiliate-partner-signups-link', function (evt) { + evt.stopPropagation(); + renderAffiliateTab('signups'); + return false; + }); + $("#account-affiliate-partner-content-scroller").on('click', '#affiliate-partner-earnings-link', function (evt) { + evt.stopPropagation(); + renderAffiliateTab('earnings'); + return false; + }); + $("#account-affiliate-partner-content-scroller").on('click', '#affiliate-profile-account-submit', function (evt) { + evt.stopPropagation(); + handleUpdateAffiliateAccount(); + return false; + }); + } + + function _renderAffiliateTableSignups(rows) { + rest.getAffiliateSignups() + .done(onAffiliateSignups) + .fail(app.ajaxError) + } + + function _renderAffiliateTableEarnings(rows) { + rest.getAffiliatePayments() + .done(onAffiliatePayments) + .fail(app.ajaxError) + } + + function _renderAffiliateTableLinks(rows) { + $screen.find('.affiliate-agreement').on('click', function () { + renderAffiliateTab('agreement'); + return false; + }) + + $screen.find('.affiliate-link-page').attr('href', '/affiliate/links/' + affiliatePartnerData.account.id) + + $screen.find('select.link_type').easyDropDown(); + + var $linkType = $screen.find('.link_type') + + $linkType.on('change', function() { + logger.debug("link type changed") + updateLinks(); + }) + + updateLinks(); + } + + function onAffiliateSignups(signups) { + + var $table = $screen.find('table.traffic-table tbody') + $table.empty(); + + var template = $('#template-affiliate-partner-signups-row').html(); + context._.each(signups.traffics, function(item) { + var $link = $(context._.template(template, item, {variable: 'data'})); + + var $day = $link.find('td.day') + + var day = $day.text(); + var bits = day.split('-') + if(bits.length == 3) { + $day.text(context.JK.getMonth(new Number(bits[1]) - 1) + ' ' + new Number(bits[2])) + } + + $table.append($link) + }) + } + + function onAffiliatePayments(payments) { + var $table = $screen.find('table.payment-table tbody') + $table.empty(); + + var template = $('#template-affiliate-partner-earnings-row').html(); + context._.each(payments.payments, function(item) { + + var data = {} + if(item.payment_type == 'quarterly') { + + if(item.quarter == 0) { + data.time = '1st Quarter ' + item.year + } + else if(item.quarter == 1) { + data.time = '2nd Quarter ' + item.year + } + else if(item.quarter == 2) { + data.time = '3rd Quarter ' + item.year + } + else if(item.quarter == 3) { + data.time = '4th Quarter ' + item.year + } + + data.sold = '' + + if(item.paid) { + data.earnings = 'PAID $' + (item.due_amount_in_cents / 100).toFixed(2); + } + else { + data.earnings = 'No earning were paid, as the $10 minimum threshold was not reached.' + } + } + else { + data.time = context.JK.getMonth(item.month - 1) + ' ' + item.year; + if(item.jamtracks_sold == 1) { + data.sold = 'JamTracks: ' + item.jamtracks_sold + ' unit sold'; + } + else { + data.sold = 'JamTracks: ' + item.jamtracks_sold + ' units sold'; + } + data.earnings = '$' + (item.due_amount_in_cents / 100).toFixed(2); + } + + + var $earning = $(context._.template(template, data, {variable: 'data'})); + + $table.append($earning) + }) + } + + + function updateLinks() { + var $select = $screen.find('select.link_type') + var value = $select.val() + + logger.debug("value: " + value) + + var type = 'jamtrack_songs'; + if(value == 'JamTrack Song') { + type = 'jamtrack_songs' + } + else if(value == 'JamTrack Band') { + type = 'jamtrack_bands' + } + else if(value == 'JamTrack General') { + type = 'jamtrack_general' + } + else if(value == 'JamKazam General') { + type = 'jamkazam' + } + else if(value == 'JamKazam Session') { + type = 'sessions' + } + else if(value == 'JamKazam Recording') { + type = 'recordings' + } + else if(value == 'Custom Link') { + type = 'custom_links' + } + + $screen.find('.link-type-prompt').hide(); + $screen.find('.link-type-prompt[data-type="' + type + '"]').show(); + + if(type == 'custom_links') { + $screen.find('table.links-table').hide(); + $screen.find('.link-type-prompt[data-type="custom_links"] span.affiliate_id').text(affiliatePartnerData.account.id) + } + else { + rest.getLinks(type) + .done(populateLinkTable) + .fail(function() { + app.notify({message: 'Unable to fetch links. Please try again later.' }) + }) + } + } + + function _renderAffiliateTab(theTab) { + affiliateTabRefresh(theTab); + var template = $('#template-affiliate-partner-' + theTab).html(); + var tabHtml = context._.template(template, affiliatePartnerData[theTab], {variable: 'data'}); + $('#affiliate-partner-tab-content').html(tabHtml); + + if (theTab == 'signups') { + _renderAffiliateTableSignups(affiliatePartnerData[theTab]); + } else if (theTab == 'earnings') { + _renderAffiliateTableEarnings(affiliatePartnerData[theTab]); + } else if (theTab == 'links') { + _renderAffiliateTableLinks(affiliatePartnerData[theTab]); + } + } + + function renderAffiliateTab(theTab) { + if (affiliatePartnerData) { + return _renderAffiliateTab(theTab); + } + rest.getAffiliatePartnerData(userId) + .done(function (response) { + affiliatePartnerData = response; + _renderAffiliateTab(theTab); + }) + .fail(app.ajaxError) + } + + function affiliateTabRefresh(selectedTab) { + var container = $('#account-affiliate-partner-content-scroller'); + container.find('.affiliate-partner-nav a.active').removeClass('active'); + if (selectedTab) { + container.find('.affiliate-partner-' + selectedTab).show(); + $.each(affiliatePartnerTabs, function (index, val) { + if (val != selectedTab) { + container.find('.affiliate-partner-' + val).hide(); + } + }); + container.find('.affiliate-partner-nav a#affiliate-partner-' + selectedTab + '-link').addClass('active'); + } else { + $.each(affiliatePartnerTabs, function (index, val) { + container.find('.affiliate-partner-' + val).hide(); + }); + container.find('.affiliate-partner-nav a#affiliate-partner-' + affiliatePartnerTabs[0] + '-link').addClass('active'); + } + } + + function handleUpdateAffiliateAccount() { + var tab_content = $('#affiliate-partner-tab-content'); + var affiliate_partner_data = { + 'address': { + 'address1': tab_content.find('#affiliate_partner_address1').val(), + 'address2': tab_content.find('#affiliate_partner_address2').val(), + 'city': tab_content.find('#affiliate_partner_city').val(), + 'state': tab_content.find('#affiliate_partner_state').val(), + 'postal_code': tab_content.find('#affiliate_partner_postal_code').val(), + 'country': tab_content.find('#affiliate_partner_country').val() + }, + 'tax_identifier': tab_content.find('#affiliate_partner_tax_identifier').val() + } + rest.postAffiliatePartnerData(userId, affiliate_partner_data) + .done(postUpdateAffiliateAccountSuccess); + } + + function postUpdateAffiliateAccountSuccess(response) { + app.notify( + { + title: "Affiliate Account", + text: "You have updated your affiliate partner data successfully." + }, + null, true); + } + + function populateLinkTable(response) { + $screen.find('table.links-table').show(); + var $linkTable = $screen.find('.links-table tbody') + + $linkTable.empty(); + var template = $('#template-affiliate-link-entry').html(); + context._.each(response, function(item) { + var $link = $(context._.template(template, item, {variable: 'data'})); + $link.find('td.copy-link a').click(copyLink) + $linkTable.append($link) + }) + } + + function copyLink() { + var element = $(this); + var $url = element.closest('tr').find('td.url input') + $url.select() + + return false; + } + + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow + }; + app.bindScreen('account/affiliatePartner', screenBindings); + $screen = $('#account-affiliate-partner') + events(); + } + + this.initialize = initialize; + this.beforeShow = beforeShow; + this.afterShow = afterShow; + return this; + }; + +})(window, jQuery); diff --git a/web/app/assets/javascripts/accounts_audio_profile.js b/web/app/assets/javascripts/accounts_audio_profile.js index 1e646a4cf..a84c521bb 100644 --- a/web/app/assets/javascripts/accounts_audio_profile.js +++ b/web/app/assets/javascripts/accounts_audio_profile.js @@ -163,6 +163,13 @@ function handleConfigureAudioProfile(audioProfileId) { + if(!gearUtils.canBeConfigured(audioProfileId)) { + + context.JK.Banner.showAlert("The System Default (Playback Only) profile can not be configured."); + return; + } + + if(audioProfileId == gearUtils.GearUtil) if(audioProfileId != context.jamClient.FTUEGetMusicProfileName()) { logger.debug("activating " + audioProfileId); var result = context.jamClient.FTUELoadAudioConfiguration(audioProfileId); diff --git a/web/app/assets/javascripts/accounts_jamtracks.js.coffee b/web/app/assets/javascripts/accounts_jamtracks.js.coffee index 43e1e51b9..74706efe1 100644 --- a/web/app/assets/javascripts/accounts_jamtracks.js.coffee +++ b/web/app/assets/javascripts/accounts_jamtracks.js.coffee @@ -9,6 +9,7 @@ context.JK.AccountJamTracks = class AccountJamTracks @logger = context.JK.logger @screen = null @userId = context.JK.currentUserId; + @sessionUtils = context.JK.SessionUtils initialize:() => screenBindings = @@ -18,48 +19,46 @@ context.JK.AccountJamTracks = class AccountJamTracks @screen = $('#account-jamtracks') beforeShow:() => - @logger.debug("beforeShow") rest.getPurchasedJamTracks({}) .done(@populateJamTracks) .fail(@app.ajaxError); afterShow:() => - @logger.debug("afterShow") - - populateJamTracks:(data) => - @logger.debug("populateJamTracks", data) - template = context._.template($('#template-account-jamtrack').html(), {jamtracks:data.jamtracks}, { variable: 'data' }) - - # template = context._.template($('#template-account-jamtrack').html(), { - # jamtracks: data.jamtracks - # current_user: @userId - # }, variable: 'data') - @logger.debug("TEMPLATE", template) - this.appendJamTracks template - @screen.find('.jamtrack-solo-session').on 'click', @soloSession - @screen.find('.jamtrack-group-session').on 'click', @groupSession - appendJamTracks:(template) => - $('#account-my-jamtracks table tbody').replaceWith template + populateJamTracks:(data) => + if (data.jamtracks? && data.jamtracks.length > 0) + @screen.find(".no-jamtracks-found").addClass("hidden") + @appendJamTracks(data) + @screen.find('.jamtrack-solo-session').on 'click', @soloSession + @screen.find('.jamtrack-group-session').on 'click', @groupSession + else + @screen.find(".no-jamtracks-found").removeClass("hidden") + + appendJamTracks:(data) => + + $tbody = $('#account-my-jamtracks table tbody') + $tbody.empty() + + for jamTrack in data.jamtracks + $template = $(context._.template($('#template-account-jamtrack').html(), {jamtrack:jamTrack}, { variable: 'data' })) + $template.data('jamTrack', jamTrack) + $tbody.append($template) + soloSession:(e) => #context.location="client#/createSession" - @logger.debug "BLEH", e jamRow = $(e.target).parents("tr") - @logger.debug "BLEH2", e, jamRow.data() - @createSession(jamRow.data(), true) - #@logger.debug "BLEH", $(this), $(this).data() + @createSession(jamRow.data(), true, jamRow.data('jamTrack')) groupSession:(e) => #context.location="client#/createSession" jamRow = $(e.target).parents("tr") - @createSession(jamRow.data(), false) + @createSession(jamRow.data(), false, jamRow.data('jamTrack')) - createSession:(sessionData, solo) => + createSession:(sessionData, solo, jamTrack) => tracks = context.JK.TrackHelpers.getUserTracks(context.jamClient) if (context.JK.guardAgainstBrowser(@app)) - @logger.debug("CRATING SESSION", sessionData.genre, solo) data = {} data.client_id = @app.clientId #data.description = $('#description').val() @@ -85,6 +84,7 @@ context.JK.AccountJamTracks = class AccountJamTracks rest.legacyCreateSession(data).done((response) => newSessionId = response.id + @sessionUtils.setAutoOpenJamTrack(jamTrack) # so that the session screen will pick this up context.location = '/client#/session/' + newSessionId # Re-loading the session settings will cause the form to reset with the right stuff in it. # This is an extra xhr call, but it keeps things to a single codepath diff --git a/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee b/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee new file mode 100644 index 000000000..82dbe73eb --- /dev/null +++ b/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee @@ -0,0 +1,171 @@ +$ = jQuery +context = window +context.JK ||= {} + +context.JK.AccountPaymentHistoryScreen = class AccountPaymentHistoryScreen + LIMIT = 20 + + constructor: (@app) -> + @logger = context.JK.logger + @rest = context.JK.Rest() + @screen = null + @scroller = null + @genre = null + @artist = null + @instrument = null + @availability = null + @nextPager = null + @noMoreSales = null + @currentPage = 0 + @next = null + @tbody = null + @rowTemplate = null + + beforeShow:(data) => + + + afterShow:(data) => + @refresh() + + events:() => + @backBtn.on('click', @onBack) + + onBack:() => + window.location = '/client#/account' + return false + + clearResults:() => + @currentPage = 0 + @tbody.empty() + @noMoreSales.hide() + @next = null + + + + refresh:() => + @currentQuery = this.buildQuery() + @rest.getSalesHistory(@currentQuery) + .done(@salesHistoryDone) + .fail(@salesHistoryFail) + + + renderPayments:(response) => + if response.entries? && response.entries.length > 0 + for paymentHistory in response.entries + if paymentHistory.sale? + # this is a sale + sale = paymentHistory.sale + amt = sale.recurly_total_in_cents + status = 'paid' + displayAmount = ' $' + (amt/100).toFixed(2) + date = context.JK.formatDate(sale.created_at, true) + items = [] + for line_item in sale.line_items + items.push(line_item.product_info?.name) + description = items.join(', ') + else + # this is a recurly webhook + transaction = paymentHistory.transaction + amt = transaction.amount_in_cents + status = transaction.transaction_type + displayAmount = '($' + (amt/100).toFixed(2) + ')' + date = context.JK.formatDate(transaction.transaction_at, true) + description = transaction.admin_description + + payment = { + date: date + amount: displayAmount + status: status + payment_method: 'Credit Card' + description: description + } + + tr = $(context._.template(@rowTemplate, payment, { variable: 'data' })); + @tbody.append(tr); + else + tr = "No payments found" + @tbody.append(tr); + + salesHistoryDone:(response) => + + # Turn in to HTML rows and append: + #@tbody.html("") + @next = response.next + @renderPayments(response) + if response.next == null + # if we less results than asked for, end searching + @scroller.infinitescroll 'pause' + @logger.debug("end of history") + if @currentPage > 0 + @noMoreSales.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++ + this.buildQuery() + this.registerInfiniteScroll() + + + salesHistoryFail:(jqXHR)=> + @noMoreSales.show() + @app.notifyServerError jqXHR, 'Payment History Unavailable' + + defaultQuery:() => + query = + per_page: LIMIT + page: @currentPage+1 + if @next + query.since = @next + query + + buildQuery:() => + @currentQuery = this.defaultQuery() + + + registerInfiniteScroll:() => + that = this + @scroller.infinitescroll { + behavior: 'local' + navSelector: '#account-payment-history .btn-next-pager' + nextSelector: '#account-payment-history .btn-next-pager' + binder: @scroller + dataType: 'json' + appendCallback: false + prefill: false + bufferPx: 100 + loading: + msg: $('
Loading ...
') + img: '/assets/shared/spinner.gif' + path: (page) => + '/api/payment_histories?' + $.param(that.buildQuery()) + + }, (json, opts) => + this.salesHistoryDone(json) + @scroller.infinitescroll 'resume' + + initialize:() => + screenBindings = + 'beforeShow': this.beforeShow + 'afterShow': this.afterShow + @app.bindScreen 'account/paymentHistory', screenBindings + @screen = $('#account-payment-history') + @scroller = @screen.find('.content-body-scroller') + @nextPager = @screen.find('a.btn-next-pager') + @noMoreSales = @screen.find('.end-of-payments-list') + @tbody = @screen.find("table.payment-table tbody") + @rowTemplate = $('#template-payment-history-row').html() + @backBtn = @screen.find('.back') + + if @screen.length == 0 + throw new Error('@screen must be specified') + if @scroller.length == 0 + throw new Error('@scroller must be specified') + if @tbody.length == 0 + throw new Error('@tbody must be specified') + if @noMoreSales.length == 0 + throw new Error('@noMoreSales must be specified') + + this.events() + + diff --git a/web/app/assets/javascripts/accounts_video_profile.js b/web/app/assets/javascripts/accounts_video_profile.js new file mode 100644 index 000000000..fbb1a6d53 --- /dev/null +++ b/web/app/assets/javascripts/accounts_video_profile.js @@ -0,0 +1,32 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.AccountVideoProfile = function (app) { + var $webcamViewer = new context.JK.WebcamViewer() + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'beforeHide':beforeHide + }; + app.bindScreen('account/video', screenBindings); + + $webcamViewer.init($(".webcam-container")) + } + + function beforeShow() { + $webcamViewer.beforeShow() + } + + function beforeHide() { + $webcamViewer.setVideoOff() + } + + this.beforeShow = beforeShow + this.beforeHide = beforeHide + this.initialize = initialize + return this; + }; + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index bc065cf22..958491cb7 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -10,6 +10,7 @@ // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD // GO AFTER THE REQUIRES BELOW. // +//= require bugsnag //= require bind-polyfill //= require jquery //= require jquery.monkeypatch @@ -36,6 +37,9 @@ //= require jquery.custom-protocol //= require jquery.exists //= require jquery.payment +//= require jquery.visible +//= require fluxxor +//= require react-components //= require howler.core.js //= require jstz //= require class @@ -44,12 +48,17 @@ //= require globals //= require AAB_message_factory //= require jam_rest +//= require ga //= require utils //= require subscription_utils //= require custom_controls +//= require web/signup_helper +//= require web/signin_helper +//= require web/signin +//= require web/tracking //= require_directory . //= require_directory ./dialog //= require_directory ./wizard //= require_directory ./wizard/gear //= require_directory ./wizard/loopback -//= require everywhere/everywhere \ No newline at end of file +//= require everywhere/everywhere diff --git a/web/app/assets/javascripts/backend_alerts.js b/web/app/assets/javascripts/backend_alerts.js index d1c9628e1..da48d9138 100644 --- a/web/app/assets/javascripts/backend_alerts.js +++ b/web/app/assets/javascripts/backend_alerts.js @@ -69,8 +69,10 @@ logger.debug("alert callback", type, text); - var alertData = ALERT_TYPES[type]; - if(alertData) { + var alertData = $.extend({}, ALERT_TYPES[type]); + + alertData.text = text; + if(alertData && alertData.name) { $document.triggerHandler(alertData.name, alertData); } @@ -118,6 +120,10 @@ if(context.JK.CurrentSessionModel) context.JK.CurrentSessionModel.onBroadcastStopped(type, text); } + else if(type === ALERT_NAMES.RECORD_PLAYBACK_STATE) { + if(context.JK.CurrentSessionModel) + context.JK.CurrentSessionModel.onPlaybackStateChange(type, text); + } else if((!context.JK.CurrentSessionModel || !context.JK.CurrentSessionModel.inSession()) && (ALERT_NAMES.INPUT_IO_RATE == type || ALERT_NAMES.INPUT_IO_JTR == type || ALERT_NAMES.OUTPUT_IO_RATE == type || ALERT_NAMES.OUTPUT_IO_JTR== type)) { // squelch these events if not in session diff --git a/web/app/assets/javascripts/checkout_complete.js b/web/app/assets/javascripts/checkout_complete.js new file mode 100644 index 000000000..4f1e96110 --- /dev/null +++ b/web/app/assets/javascripts/checkout_complete.js @@ -0,0 +1,215 @@ +(function (context, $) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.CheckoutCompleteScreen = function (app) { + + var EVENTS = context.JK.EVENTS; + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var jamTrackUtils = context.JK.JamTrackUtils; + var checkoutUtils = context.JK.CheckoutUtilsInstance; + + var $screen = null; + var $navigation = null; + var $templatePurchasedJamTrack = null; + var $thanksPanel = null; + var $jamTrackInBrowser = null; + var $purchasedJamTrack = null; + var $purchasedJamTrackHeader = null; + var $purchasedJamTracks = null; + var userDetail = null; + var step = null; + var downloadJamTracks = []; + var purchasedJamTracks = null; + var purchasedJamTrackIterator = 0; + var $backBtn = null; + var $downloadApplicationLink = null; + var $noPurchasesPrompt = null; + + + function beforeShow() { + + } + + + function afterShow(data) { + prepThanks() + } + + + function beforeHide() { + if(downloadJamTracks) { + context._.each(downloadJamTracks, function(downloadJamTrack) { + downloadJamTrack.destroy(); + downloadJamTrack.root.remove(); + }) + + downloadJamTracks = []; + } + purchasedJamTracks = null; + purchasedJamTrackIterator = 0; + } + + function prepThanks() { + $noPurchasesPrompt.addClass('hidden') + $purchasedJamTracks.empty() + $thanksPanel.addClass("hidden") + $purchasedJamTrackHeader.attr('status', 'in-progress') + step = 3; + renderNavigation(); + showThanks(); + } + + + function showThanks(purchaseResponse) { + + + var purchaseResponse = checkoutUtils.getLastPurchase(); + + if(!purchaseResponse || purchaseResponse.length == 0) { + // user got to this page with no context + logger.debug("no purchases found; nothing to show") + $noPurchasesPrompt.removeClass('hidden') + } + else { + $thanksPanel.removeClass('hidden') + handleJamTracksPurchased(purchaseResponse.jam_tracks) + } + } + + function handleJamTracksPurchased(jamTracks) { + // were any JamTracks purchased? + var jamTracksPurchased = jamTracks && jamTracks.length > 0; + if(jamTracksPurchased) { + if(gon.isNativeClient) { + startDownloadJamTracks(jamTracks) + } + else { + $jamTrackInBrowser.removeClass('hidden'); + app.user().done(function(user) { + if(!user.first_downloaded_client_at) { + $downloadApplicationLink.removeClass('hidden') + } + }) + } + } + } + + function startDownloadJamTracks(jamTracks) { + // there can be multiple purchased JamTracks, so we cycle through them + + purchasedJamTracks = jamTracks; + + // populate list of jamtracks purchased, that we will iterate through graphically + context._.each(jamTracks, function(jamTrack) { + var downloadJamTrack = new context.JK.DownloadJamTrack(app, jamTrack, 'small'); + var $purchasedJamTrack = $(context._.template( + $templatePurchasedJamTrack.html(), + jamTrack, + {variable: 'data'} + )); + + $purchasedJamTracks.append($purchasedJamTrack) + + // show it on the page + $purchasedJamTrack.append(downloadJamTrack.root) + + downloadJamTracks.push(downloadJamTrack) + }) + + iteratePurchasedJamTracks(); + } + + function iteratePurchasedJamTracks() { + if(purchasedJamTrackIterator < purchasedJamTracks.length ) { + var downloadJamTrack = downloadJamTracks[purchasedJamTrackIterator++]; + + // make sure the 'purchasing JamTrack' section can be seen + $purchasedJamTrack.removeClass('hidden'); + + // the widget indicates when it gets to any transition; we can hide it once it reaches completion + $(downloadJamTrack).on(EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, function(e, data) { + + if(data.state == downloadJamTrack.states.synchronized) { + logger.debug("jamtrack " + downloadJamTrack.jamTrack.name + " synchronized;") + //downloadJamTrack.root.remove(); + downloadJamTrack.destroy(); + + // go to the next JamTrack + iteratePurchasedJamTracks() + } + }) + + logger.debug("jamtrack " + downloadJamTrack.jamTrack.name + " downloader initializing") + + // kick off the download JamTrack process + downloadJamTrack.init() + + // XXX style-test code + // downloadJamTrack.transitionError("package-error", "The server failed to create your package.") + + } + else { + logger.debug("done iterating over purchased JamTracks") + $purchasedJamTrackHeader.attr('status', 'done') + } + } + + function clearOrderPage() { + $orderContent.empty(); + } + + function renderNavigation() { + $navigation.html(""); + var navigationHtml = $( + context._.template( + $('#template-checkout-navigation').html(), + {current: step, purchases_disable_class: gon.global.purchases_enabled ? 'hidden' : ''}, + {variable: 'data'} + ) + ); + + $navigation.append(navigationHtml); + } + + function events() { + $backBtn.on('click', function(e) { + e.preventDefault(); + + context.location = '/client#/checkoutOrder' + }) + } + + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow, + 'beforeHide': beforeHide + }; + app.bindScreen('checkoutComplete', screenBindings); + + $screen = $("#checkoutCompleteScreen"); + $navigation = $screen.find(".checkout-navigation-bar"); + $templatePurchasedJamTrack = $('#template-purchased-jam-track'); + $thanksPanel = $screen.find(".thanks-panel"); + $jamTrackInBrowser = $screen.find(".thanks-detail.jam-tracks-in-browser"); + $purchasedJamTrack = $thanksPanel.find(".thanks-detail.purchased-jam-track"); + $purchasedJamTrackHeader = $purchasedJamTrack.find(".purchased-jam-track-header"); + $purchasedJamTracks = $purchasedJamTrack.find(".purchased-list") + $backBtn = $screen.find('.back'); + $downloadApplicationLink = $screen.find('.download-jamkazam-wrapper'); + $noPurchasesPrompt = $screen.find('.no-purchases-prompt') + + 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/checkout_order.js b/web/app/assets/javascripts/checkout_order.js index 9024f5745..168b965d6 100644 --- a/web/app/assets/javascripts/checkout_order.js +++ b/web/app/assets/javascripts/checkout_order.js @@ -8,23 +8,15 @@ var logger = context.JK.logger; var rest = context.JK.Rest(); var jamTrackUtils = context.JK.JamTrackUtils; + var checkoutUtils = context.JK.CheckoutUtilsInstance; var $screen = null; var $navigation = null; var $templateOrderContent = null; - var $templatePurchasedJamTrack = null; var $orderPanel = null; - var $thanksPanel = null; - var $jamTrackInBrowser = null; - var $purchasedJamTrack = null; - var $purchasedJamTrackHeader = null; - var $purchasedJamTracks = null; var $orderContent = null; var userDetail = null; var step = null; - var downloadJamTracks = []; - var purchasedJamTracks = null; - var purchasedJamTrackIterator = 0; var $backBtn = null; var $orderPrompt = null; var $emptyCartPrompt = null; @@ -32,26 +24,17 @@ function beforeShow() { - beforeShowOrder(); + } function afterShow(data) { - + beforeShowOrder(); } function beforeHide() { - if(downloadJamTracks) { - context._.each(downloadJamTracks, function(downloadJamTrack) { - downloadJamTrack.destroy(); - downloadJamTrack.root.remove(); - }) - downloadJamTracks = []; - } - purchasedJamTracks = null; - purchasedJamTrackIterator = 0; } function beforeShowOrder() { @@ -59,7 +42,6 @@ $emptyCartPrompt.addClass('hidden') $noAccountInfoPrompt.addClass('hidden') $orderPanel.removeClass("hidden") - $thanksPanel.addClass("hidden") $screen.find(".place-order").addClass('disabled').off('click', placeOrder) $("#order_error").text('').addClass("hidden") step = 3; @@ -99,7 +81,7 @@ var sub_total = 0.0 var taxes = 0.0 $.each(carts, function(index, cart) { - sub_total += parseFloat(cart.product_info.total_price) + sub_total += parseFloat(cart.product_info.real_price) }); if(carts.length == 0) { data.grand_total = '-.--' @@ -136,79 +118,87 @@ $orderPrompt.addClass('hidden') $emptyCartPrompt.removeClass('hidden') $noAccountInfoPrompt.addClass('hidden') - $placeOrder.addClass('disabled') + $placeOrder.addClass('disabled').unbind('click').on('click', false) } else { logger.debug("cart has " + carts.length + " items in it") $orderPrompt.removeClass('hidden') $emptyCartPrompt.addClass('hidden') $noAccountInfoPrompt.addClass('hidden') - $placeOrder.removeClass('disabled').on('click', placeOrder) + if(gon.global.purchases_enabled || context.JK.currentUserAdmin) { + $placeOrder.removeClass('disabled').unbind('click').on('click', placeOrder) + } + else { + $placeOrder.addClass('disabled').unbind('click').on('click', false) + } + estimateTaxes(carts, recurlyAccountInfo.billing_info); + } + } - var planPricing = {} + function displayTax(effectiveQuantity, item_tax, total_with_tax) { + var totalTax = 0; + var totalPrice = 0; - context._.each(carts, function(cart) { - var priceElement = $screen.find('.order-right-page .plan[data-plan-code="' + cart.product_info.plan_code +'"]') + var unitTax = item_tax * effectiveQuantity; + totalTax += unitTax; - if(priceElement.length == 0) { - logger.error("unable to find price element for " + cart.product_info.plan_code, cart); - app.notify({title: "Error Encountered", text: "Unable to find plan info for " + cart.product_info.plan_code}) - return false; - } + var totalUnitPrice = total_with_tax * effectiveQuantity; + totalPrice += totalUnitPrice; - logger.debug("creating recurly pricing element for plan: " + cart.product_info.plan_code) - var pricing = context.recurly.Pricing(); - pricing.plan_code = cart.product_info.plan_code; - pricing.resolved = false; - pricing.effective_quantity = cart.product_info.quantity - cart.product_info.marked_for_redeem - planPricing[pricing.plan_code] = pricing; + $screen.find('.order-right-page .order-items-value.taxes').text('$' + totalTax.toFixed(2)) + $screen.find('.order-right-page .order-items-value.grand-total').text('$' + totalPrice.toFixed(2)) + } - // this is called when the plan is resolved against Recurly. It will have tax info, which is the only way we can get it. - pricing.on('change', function(price) { + function estimateTaxes(carts, billing_info) { + var planPricing = {} - var resolvedPrice = planPricing[this.plan_code]; - if(!resolvedPrice) { - logger.error("unable to find price info in storage") - app.notify({title: "Error Encountered", text: "Unable to find plan info in storage"}) - return; - } - else { - logger.debug("pricing resolved for plan: " + this.plan_code) - } - resolvedPrice.resolved = true; + var priceElement = $screen.find('.order-right-page .plan.jamtrack') - var allResolved = true; - var totalTax = 0; - var totalPrice = 0; + if(priceElement.length == 0) { + logger.error("unable to find price element for jamtrack"); + app.notify({title: "Error Encountered", text: "Unable to find plan info for jam track"}) + return false; + } + logger.debug("creating recurly pricing element for plan: " + gon.recurly_tax_estimate_jam_track_plan) - // let's see if all plans have been resolved via API; and add up total price and taxes for display - $.each(planPricing, function(plan_code, priceObject) { - logger.debug("resolved recurly priceObject", priceObject) + var effectiveQuantity = 0 - if(!priceObject.resolved) { - allResolved = false; - return false; - } - else { - var unitTax = Number(priceObject.price.now.tax) * priceObject.effective_quantity; - totalTax += unitTax; + context._.each(carts, function(cart) { + effectiveQuantity += cart.product_info.quantity - cart.product_info.marked_for_redeem + }) - var totalUnitPrice = Number(priceObject.price.now.total) * priceObject.effective_quantity; - totalPrice += totalUnitPrice; - } - }) + if (gon.global.estimate_taxes) { + var state = billing_info.state; + var country = billing_info.country; + if (state) { + state = state.toLowerCase(); + } + if (country) { + country = country.toLowerCase(); + } + var taxRate = 0; + if (state && country && (state == 'tx' || state == 'texas') && country == 'us') { + taxRate = 0.0825; + } + + var unitTax = 1.99 * taxRate; + displayTax(effectiveQuantity, unitTax, 1.99 + unitTax) + } + else { + checkoutUtils.configureRecurly() + var pricing = context.recurly.Pricing(); + pricing.plan_code = gon.recurly_tax_estimate_jam_track_plan; + pricing.resolved = false; + pricing.effective_quantity = 1 + + // this is called when the plan is resolved against Recurly. It will have tax info, which is the only way we can get it. + pricing.on('change', function(price) { + logger.debug("pricing", pricing) + displayTax(effectiveQuantity, Number(pricing.price.now.tax), Number(pricing.price.now.total)); - if(allResolved) { - $screen.find('.order-right-page .order-items-value.taxes').text('$' + totalTax.toFixed(2)) - $screen.find('.order-right-page .order-items-value.grand-total').text('$' + totalPrice.toFixed(2)) - } - else - { - logger.debug("still waiting on more plans to resolve") - } - }) - pricing.attach(priceElement.eq(0)) }) + + pricing.attach(priceElement.eq(0)) } } @@ -242,85 +232,18 @@ } function moveToThanks(purchaseResponse) { + checkoutUtils.setLastPurchase(purchaseResponse) + checkoutUtils.deletePreserveBillingInfo() $("#order_error").addClass("hidden") $orderPanel.addClass("hidden") - $thanksPanel.removeClass("hidden") + checkoutUtils.deletePreserveBillingInfo() + //$thanksPanel.removeClass("hidden") jamTrackUtils.checkShoppingCart() - handleJamTracksPurchased(purchaseResponse.jam_tracks) + app.refreshUser() + + window.location = '/client#/checkoutComplete' } - function handleJamTracksPurchased(jamTracks) { - // were any JamTracks purchased? - var jamTracksPurchased = jamTracks && jamTracks.length > 0; - if(jamTracksPurchased) { - if(gon.isNativeClient) { - startDownloadJamTracks(jamTracks) - } - else { - $jamTrackInBrowser.removeClass('hidden'); - } - } - } - - function startDownloadJamTracks(jamTracks) { - // there can be multiple purchased JamTracks, so we cycle through them - - purchasedJamTracks = jamTracks; - - // populate list of jamtracks purchased, that we will iterate through graphically - context._.each(jamTracks, function(jamTrack) { - var downloadJamTrack = new context.JK.DownloadJamTrack(app, jamTrack, 'small'); - var $purchasedJamTrack = $(context._.template( - $templatePurchasedJamTrack.html(), - jamTrack, - {variable: 'data'} - )); - - $purchasedJamTracks.append($purchasedJamTrack) - - // show it on the page - $purchasedJamTrack.append(downloadJamTrack.root) - - downloadJamTracks.push(downloadJamTrack) - }) - - iteratePurchasedJamTracks(); - } - - function iteratePurchasedJamTracks() { - if(purchasedJamTrackIterator < purchasedJamTracks.length ) { - var downloadJamTrack = downloadJamTracks[purchasedJamTrackIterator++]; - - // make sure the 'purchasing JamTrack' section can be seen - $purchasedJamTrack.removeClass('hidden'); - - // the widget indicates when it gets to any transition; we can hide it once it reaches completion - $(downloadJamTrack).on(EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, function(e, data) { - - if(data.state == downloadJamTrack.states.synchronized) { - logger.debug("jamtrack " + downloadJamTrack.jamTrack.name + " synchronized;") - //downloadJamTrack.root.remove(); - downloadJamTrack.destroy(); - - // go to the next JamTrack - iteratePurchasedJamTracks() - } - }) - - logger.debug("jamtrack " + downloadJamTrack.jamTrack.name + " downloader initializing") - - // kick off the download JamTrack process - downloadJamTrack.init() - - // XXX style-test code - // downloadJamTrack.transitionError("package-error", "The server failed to create your package.") - - } - else { - logger.debug("done iterating over purchased JamTracks") - $purchasedJamTrackHeader.text('All purchased JamTracks have been downloaded successfully! You can now play them in a session.') - } - } function clearOrderPage() { $orderContent.empty(); @@ -331,7 +254,7 @@ var navigationHtml = $( context._.template( $('#template-checkout-navigation').html(), - {current: step}, + {current: step, purchases_disable_class: gon.global.purchases_enabled ? 'hidden' : ''}, {variable: 'data'} ) ); @@ -358,13 +281,7 @@ $screen = $("#checkoutOrderScreen"); $navigation = $screen.find(".checkout-navigation-bar"); $templateOrderContent = $("#template-order-content"); - $templatePurchasedJamTrack = $('#template-purchased-jam-track'); $orderPanel = $screen.find(".order-panel"); - $thanksPanel = $screen.find(".thanks-panel"); - $jamTrackInBrowser = $screen.find(".thanks-detail.jam-tracks-in-browser"); - $purchasedJamTrack = $thanksPanel.find(".thanks-detail.purchased-jam-track"); - $purchasedJamTrackHeader = $purchasedJamTrack.find(".purchased-jam-track-header"); - $purchasedJamTracks = $purchasedJamTrack.find(".purchased-list") $backBtn = $screen.find('.back'); $orderPrompt = $screen.find('.order-prompt'); $emptyCartPrompt = $screen.find('.empty-cart-prompt'); diff --git a/web/app/assets/javascripts/checkout_payment.js b/web/app/assets/javascripts/checkout_payment.js index 7c40ced26..9b72f4fc5 100644 --- a/web/app/assets/javascripts/checkout_payment.js +++ b/web/app/assets/javascripts/checkout_payment.js @@ -7,6 +7,7 @@ var EVENTS = context.JK.EVENTS; var logger = context.JK.logger; var jamTrackUtils = context.JK.JamTrackUtils; + var checkoutUtils = context.JK.CheckoutUtilsInstance; var $screen = null; var $navigation = null; @@ -30,6 +31,7 @@ var selectCountryLoaded = false; var $freeJamTrackPrompt = null; var $noFreeJamTrackPrompt = null; + var $alreadyEnteredJamTrackPrompt = null; function afterShow() { @@ -44,12 +46,26 @@ function renderAccountInfo() { + + $paymentInfoPanel.addClass('hidden') $reuseExistingCard.addClass('hidden'); $newCardInfo.removeClass('hidden'); $freeJamTrackPrompt.addClass('hidden'); $noFreeJamTrackPrompt.addClass('hidden'); + $alreadyEnteredJamTrackPrompt.addClass('hidden') $("#payment_error").addClass('hidden').text('') + + if(checkoutUtils.shouldPreserveBillingInfo()) { + logger.debug("showing 'user has already set up billing info' because 'preserve billing' session is active") + checkoutUtils.refreshPreserveBillingInfo() + $alreadyEnteredJamTrackPrompt.removeClass('hidden') + return + } + + $paymentInfoPanel.removeClass('hidden') + + var selectCountryReady = selectCountry.ready(); if(!selectCountryReady) { // one time init of country dropdown @@ -138,8 +154,6 @@ } function beforeShow(data) { - // XXX : style-test code - // moveToThanks({jam_tracks: [{id: 14, jam_track_right_id: 11, name: 'Back in Black'}, {id: 15, jam_track_right_id: 11, name: 'In Bloom'}, {id: 16, jam_track_right_id: 11, name: 'Love Bird Supreme'}]}); } function beforeHide() { @@ -152,6 +166,13 @@ // TODO: Refactor: this function is long and fraught with many return points. function next(e) { + + // check if we are showing the 'change payment info' pass; if so, just move on to checkoutOrder + if($alreadyEnteredJamTrackPrompt.is(':visible')) { + logger.debug("skipping payment logic ") + context.location = '/client#/checkoutOrder' + return false; + } $paymentInfoPanel.find('.error-text').remove(); $paymentInfoPanel.find('.error').removeClass('error'); e.preventDefault(); @@ -471,6 +492,8 @@ $screen.find("#payment-info-next").off("click"); rest.createRecurlyAccount({billing_info: billing_info, terms_of_service: terms, email: email, password: password, reuse_card_this_time: reuse_card_this_time, reuse_card_next_time: reuse_card_next_time}) .done(function() { + // so the user can hit back in checkoutOrder and not have to re-enter billing info right away + checkoutUtils.setPreserveBillingInfo(); $screen.find("#payment-info-next").on("click", next); if(isLoggedIn) { @@ -537,20 +560,6 @@ $screen.find("#payment-info-next").on('click', next); } - function beforeShowOrder() { - step = 3; - renderNavigation(); - populateOrderPage(); - } - - - function populateOrderPage() { - - rest.getShoppingCarts() - .done(renderOrderPage) - .fail(app.ajaxError); - } - function toggleShippingAsBilling(e) { e.preventDefault(); @@ -564,6 +573,20 @@ } } + function changeBillingInfo(e) { + if(e) { + e.preventDefault(); + } + + logger.debug("change billing info requested") + + // clear out the skip billing info behavior + checkoutUtils.deletePreserveBillingInfo() + + renderAccountInfo(); + + return false; + } function toggleReuseExistingCard(e) { if(e) { e.preventDefault(); @@ -594,6 +617,7 @@ $screen.find("#payment-info-next").on('click', next); $shippingAsBilling.on('ifChanged', toggleShippingAsBilling); $reuseExistingCardChk.on('ifChanged', toggleReuseExistingCard); + $alreadyEnteredJamTrackPrompt.find('.change-payment-info').on('click', changeBillingInfo) } function reset() { @@ -605,7 +629,7 @@ var navigationHtml = $( context._.template( $('#template-checkout-navigation').html(), - {current: step}, + {current: step, purchases_disable_class: gon.global.purchases_enabled ? 'hidden' : ''}, {variable: 'data'} ) ); @@ -650,6 +674,7 @@ $newCardInfo = $paymentInfoPanel.find('.new-card-info') $freeJamTrackPrompt = $screen.find('.payment-prompt.free-jamtrack') $noFreeJamTrackPrompt = $screen.find('.payment-prompt.no-free-jamtrack') + $alreadyEnteredJamTrackPrompt = $screen.find('.payment-prompt.already-entered') if($screen.length == 0) throw "$screen must be specified"; diff --git a/web/app/assets/javascripts/checkout_signin.js b/web/app/assets/javascripts/checkout_signin.js index 22ee88060..9dad91bdf 100644 --- a/web/app/assets/javascripts/checkout_signin.js +++ b/web/app/assets/javascripts/checkout_signin.js @@ -5,6 +5,7 @@ context.JK.CheckoutSignInScreen = function(app) { var logger = context.JK.logger; + var rest = context.JK.Rest(); var $screen = null; var $navigation = null; @@ -17,6 +18,8 @@ var $inputElements = null; var $contentHolder = null; var $btnNext = null; + var $btnFacebook = null; + var checkoutUtils = context.JK.CheckoutUtilsInstance; function beforeShow(data) { renderNavigation(); @@ -44,6 +47,7 @@ $signinForm.on('submit', login); $signinBtn.on('click', login); $btnNext.on('click', moveNext); + $btnFacebook.on('click', facebookSignup); } function reset() { @@ -55,6 +59,31 @@ return false; } + + function facebookSignup() { + + var $btn = $(this); + + if($btn.is('.disabled')) { + logger.debug("ignoring fast attempt at facebook signup") + return false; + } + + $btn.addClass('disabled') + + rest.createSignupHint({redirect_location: '/client#/checkoutPayment'}) + .done(function() { + // send the user on to facebook signin + window.location = $btn.attr('href'); + }) + .fail(function() { + app.notify({text:"Facebook Signup is not working properly"}); + }) + .always(function() { + $btn.removeClass('disabled') + }) + return false; + } function login() { if($signinBtn.is('.disabled')) { return false; @@ -68,9 +97,23 @@ $signinBtn.text('TRYING...').addClass('disabled') rest.login({email: email, password: password, remember_me: true}) - .done(function() { - window.location = '/client#/checkoutPayment' - window.location.reload(); + .done(function(user) { + // now determine where we should send the user + rest.getShoppingCarts() + .done(function(carts) { + if(checkoutUtils.hasOneFreeItemInShoppingCart(carts)) { + window.location = '/client#/redeemComplete' + window.location.reload(); + } + else { + window.location = '/client#/checkoutPayment' + window.location.reload(); + } + }) + .fail(function() { + window.location = '/client#/jamtrackBrowse' + window.location.reload(); + }) }) .fail(function(jqXHR) { if(jqXHR.status == 422) { @@ -93,7 +136,7 @@ var navigationHtml = $( context._.template( $('#template-checkout-navigation').html(), - {current: 1}, + {current: 1, purchases_disable_class: gon.global.purchases_enabled ? 'hidden' : ''}, {variable: 'data'} ) ); @@ -117,6 +160,7 @@ $inputElements = $signinForm.find('.input-elements'); $contentHolder = $screen.find('.content-holder'); $btnNext = $screen.find('.btnNext'); + $btnFacebook = $screen.find('.signin-facebook') if($screen.length == 0) throw "$screen must be specified"; if($navigation.length == 0) throw "$navigation must be specified"; diff --git a/web/app/assets/javascripts/checkout_utils.js.coffee b/web/app/assets/javascripts/checkout_utils.js.coffee new file mode 100644 index 000000000..4570bd052 --- /dev/null +++ b/web/app/assets/javascripts/checkout_utils.js.coffee @@ -0,0 +1,64 @@ +$ = jQuery +context = window +context.JK ||= {}; + +class CheckoutUtils + constructor: () -> + @logger = context.JK.logger + @rest = new context.JK.Rest(); + @cookie_name = "preserve_billing" + @lastPurchaseResponse = null + @configuredRecurly = false + + init: () => + + refreshPreserveBillingInfo:() => + if @shouldPreserveBillingInfo + @logger.debug("refreshing preserve billing info timer") + @setPreserveBillingInfo() + + setPreserveBillingInfo:() => + date = new Date(); + minutes = 10; + date.setTime(date.getTime() + (minutes * 60 * 1000)) + $.removeCookie(@cookie_name, { path: '/' }) + $.cookie(@cookie_name, "jam", { expires: date, path: '/' }) + + deletePreserveBillingInfo:() => + $.removeCookie(@cookie_name, { path: '/' }) + + @logger.debug("deleted preserve billing"); + + unless $.cookie(@cookie_name)? + @logger.error("after deleting the preserve billing cookie, it still exists!") + + + # existance of cookie means we should preserve billing + shouldPreserveBillingInfo:() => + value = $.cookie(@cookie_name) + value? + + setLastPurchase: (purchaseResponse) => + @lastPurchaseResponse = purchaseResponse + + getLastPurchase: () => + return @lastPurchaseResponse + + hasOneFreeItemInShoppingCart: (carts) => + + if carts.length == 0 + # nothing is in the user's shopping cart. They shouldn't be here. + return false; + else if carts.length > 1 + # the user has multiple items in their shopping cart. They shouldn't be here. + return false; + + return carts[0].product_info.free + + configureRecurly: () => + unless @configuredRecurly + context.recurly.configure(gon.global.recurly_public_api_key) + @configuredRecurly = true + +# global instance +context.JK.CheckoutUtilsInstance = new CheckoutUtils() \ No newline at end of file diff --git a/web/app/assets/javascripts/client_init.js.coffee b/web/app/assets/javascripts/client_init.js.coffee new file mode 100644 index 000000000..20d0e76cc --- /dev/null +++ b/web/app/assets/javascripts/client_init.js.coffee @@ -0,0 +1,18 @@ +# one time init stuff for the /client view + + +$ = jQuery +context = window +context.JK ||= {}; + +context.JK.ClientInit = class ClientInit + constructor: () -> + @logger = context.JK.logger + @gearUtils = context.JK.GearUtils + + init: () => + if context.gon.isNativeClient + this.nativeClientInit() + + nativeClientInit: () => + @gearUtils.bootstrapDefaultPlaybackProfile(); diff --git a/web/app/assets/javascripts/corp/corporate.js b/web/app/assets/javascripts/corp/corporate.js index 60cef8df2..5d6e33bfa 100644 --- a/web/app/assets/javascripts/corp/corporate.js +++ b/web/app/assets/javascripts/corp/corporate.js @@ -1,3 +1,4 @@ +//= require bugsnag //= require jquery //= require jquery.queryparams //= require AAA_Log @@ -5,7 +6,7 @@ //= require globals //= require jamkazam //= require utils -//= require ga //= require jam_rest +//= require ga //= require corp/init //= require_directory ../corp \ 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 2ced92f0c..2c777c0f0 100644 --- a/web/app/assets/javascripts/dialog/banner.js +++ b/web/app/assets/javascripts/dialog/banner.js @@ -133,16 +133,25 @@ if(options.buttons) { context._.each(options.buttons, function(button, i) { if(!button.name) throw "button.name must be specified"; - if(!button.click) throw "button.click must be specified"; + if(!button.click && !button.href) throw "button.click or button.href must be specified"; - var buttonStyle = options.buttons.length == i + 1 ? 'button-orange' : 'button-grey'; + var buttonStyle = button.buttonStyle; + if(!buttonStyle) { + buttonStyle = options.buttons.length == i + 1 ? 'button-orange' : 'button-grey'; + } var $btn = $('' + button.name + ''); - $btn.click(function() { - button.click(); - hide(); - return false; - }); + + if(button.href) { + $btn.attr('href', button.href) + } + else { + $btn.click(function() { + button.click(); + hide(); + return false; + }); + } $buttons.append($btn); }); } diff --git a/web/app/assets/javascripts/dialog/clientPreferencesDialog.js b/web/app/assets/javascripts/dialog/clientPreferencesDialog.js index 308006d3e..9b4f0fa72 100644 --- a/web/app/assets/javascripts/dialog/clientPreferencesDialog.js +++ b/web/app/assets/javascripts/dialog/clientPreferencesDialog.js @@ -21,6 +21,7 @@ var autostart = $autoStartField.find('.icheckbox_minimal').is('.checked'); context.jamClient.SetAutoStart(autostart); app.layout.closeDialog('client-preferences-dialog') + context.jamClient.SaveSettings(); return false; }) } @@ -54,4 +55,4 @@ this.initialize = initialize; } -})(window, jQuery); \ No newline at end of file +})(window, jQuery); diff --git a/web/app/assets/javascripts/dialog/configureTrackDialog.js b/web/app/assets/javascripts/dialog/configureTrackDialog.js index f89efe96b..e4174df90 100644 --- a/web/app/assets/javascripts/dialog/configureTrackDialog.js +++ b/web/app/assets/javascripts/dialog/configureTrackDialog.js @@ -181,6 +181,7 @@ configureTracksHelper.reset(); } + function beforeShow() { profiles = gearUtils.getProfiles(); @@ -194,10 +195,17 @@ context.JK.alertSupportedNeeded("Unable to determine the current profile."); } + var result = context.jamClient.FTUELoadAudioConfiguration(currentProfile); + + if(!result) { + logger.error("unable to activate audio configuration: " + currentProfile + ", " + JSON.stringify(result)); + context.JK.alertSupportedNeeded("Unable to activate audio configuration for profile named: " + currentProfile); + return; + } + configureTracksHelper.reset(); voiceChatHelper.reset(); voiceChatHelper.beforeShow(); - } function afterShow() { diff --git a/web/app/assets/javascripts/dialog/gettingStartedDialog.js b/web/app/assets/javascripts/dialog/gettingStartedDialog.js index 6fc114f74..0faa28699 100644 --- a/web/app/assets/javascripts/dialog/gettingStartedDialog.js +++ b/web/app/assets/javascripts/dialog/gettingStartedDialog.js @@ -9,6 +9,9 @@ var $dialog = null; var $dontShowAgain = null; var $setupGearBtn = null; + var $browserJamTrackBtn = null; + var $jamTrackSection = null; + var $jamTracksLimitedTime = null; function handleStartAudioQualification() { @@ -45,6 +48,12 @@ return false; }) + $browserJamTrackBtn.click(function() { + app.layout.closeDialog('getting-started') + window.location = '/client#/jamtrackBrowse' + return false; + }) + $('#getting-started-dialog a.facebook-invite').on('click', function (e) { invitationDialog.showFacebookDialog(e); }); @@ -59,13 +68,21 @@ } function beforeShow() { + app.user().done(function(user) { + var jamtrackRule = user.free_jamtrack ? 'has-free-jamtrack' : 'no-free-jamtrack' + $jamTrackSection.removeClass('has-free-jamtrack').removeClass('no-free-jamtrack').addClass(jamtrackRule) + if(user.free_jamtrack) { + $jamTracksLimitedTime.removeClass('hidden') + } + }) } function beforeHide() { + var showWhatsNext = !$dontShowAgain.is(':checked') + app.user().done(function(user) { + app.updateUserModel({show_whats_next: showWhatsNext, show_whats_next_count: user.show_whats_next_count + 1}) + }) - if ($dontShowAgain.is(':checked')) { - app.updateUserModel({show_whats_next: false}) - } } function initializeButtons() { @@ -84,6 +101,9 @@ $dialog = $('#getting-started-dialog'); $dontShowAgain = $dialog.find('#show_getting_started'); $setupGearBtn = $dialog.find('.setup-gear-btn') + $browserJamTrackBtn = $dialog.find('.browse-jamtrack'); + $jamTrackSection = $dialog.find('.get-a-free-jamtrack-section') + $jamTracksLimitedTime = $dialog.find('.jamtracks-limited-time') registerEvents(); diff --git a/web/app/assets/javascripts/dialog/invitationDialog.js.erb b/web/app/assets/javascripts/dialog/invitationDialog.js.erb index b7887d9b4..7e3424736 100644 --- a/web/app/assets/javascripts/dialog/invitationDialog.js.erb +++ b/web/app/assets/javascripts/dialog/invitationDialog.js.erb @@ -195,7 +195,7 @@ var obj = { method: 'feed', link: signupUrl, - picture: 'http://www.jamkazam.com/assets/web/logo-256.png', + picture: 'https://www.jamkazam.com/assets/web/logo-256.png', name: 'Join me on JamKazam', caption: 'Play live music in real-time sessions with others over the Internet, as if in the same room.', description: '', diff --git a/web/app/assets/javascripts/dialog/openBackingTrackDialog.js b/web/app/assets/javascripts/dialog/openBackingTrackDialog.js index 87150456f..bd0d136aa 100644 --- a/web/app/assets/javascripts/dialog/openBackingTrackDialog.js +++ b/web/app/assets/javascripts/dialog/openBackingTrackDialog.js @@ -49,7 +49,6 @@ function getBackingTracks(page) { var result = context.jamClient.getBackingTrackList(); - console.log("result", result) var backingTracks = result.backing_tracks; if (!backingTracks || backingTracks.length == 0) { @@ -89,8 +88,7 @@ rest.openBackingTrack({id: context.JK.CurrentSessionModel.id(), backing_track_path: backingTrack.name}) .done(function(response) { var result = context.jamClient.SessionOpenBackingTrackFile(backingTrack.name, false); - console.log("BackingTrackPlay response: %o", result); - + // TODO: Possibly actually check the result. Investigate // what real client returns: // // if(result) { diff --git a/web/app/assets/javascripts/dialog/openJamTrackDialog.js b/web/app/assets/javascripts/dialog/openJamTrackDialog.js index 9f0fc5ce6..867314632 100644 --- a/web/app/assets/javascripts/dialog/openJamTrackDialog.js +++ b/web/app/assets/javascripts/dialog/openJamTrackDialog.js @@ -13,6 +13,8 @@ var $templateOpenJamTrackRow = null; var $downloadedTrackHelp = null; var $whatAreJamTracks = null; + var sampleRate = null; + var sampleRateForFilename = null; function emptyList() { @@ -24,18 +26,25 @@ } function beforeShow() { + + } + + function afterShow() { $dialog.data('result', null) emptyList(); resetPagination(); showing = true; + sampleRate = context.jamClient.GetSampleRate() + sampleRateForFilename = sampleRate == 48 ? '48' : '44'; + getPurchasedJamTracks(0) .done(function(data, textStatus, jqXHR) { // initialize pagination var $paginator = context.JK.Paginator.create(parseInt(jqXHR.getResponseHeader('total-entries')), perPage, 0, onPageSelected) $paginatorHolder.append($paginator); }); - } + } function afterHide() { showing = false; } @@ -58,7 +67,7 @@ options.jamTrackId = jamTrack.id; options.name = jamTrack.name; options.artist = jamTrack.original_artist; - var detail = context.jamClient.JamTrackGetTrackDetail(jamTrack.id) || {} + var detail = context.jamClient.JamTrackGetTrackDetail(jamTrack.id + '-' + sampleRateForFilename) || {} options.downloaded = detail.key_state == 'ready' ? 'Yes' : 'No' var $tr = $(context._.template($templateOpenJamTrackRow.html(), options, { variable: 'data' })); @@ -76,10 +85,11 @@ $tbody.on('click', 'tr', function(e) { var jamTrack = $(this).data('server-model'); - // tell the server we are about to start a recording + // tell the server we are about to open a jamtrack rest.openJamTrack({id: context.JK.CurrentSessionModel.id(), jam_track_id: jamTrack.id}) .done(function(response) { $dialog.data('result', {success:true, jamTrack: jamTrack}) + context.JK.CurrentSessionModel.updateSession(response); app.layout.closeDialog('open-jam-track-dialog'); }) .fail(function(jqXHR) { @@ -99,6 +109,7 @@ function initialize(){ var dialogBindings = { 'beforeShow' : beforeShow, + 'afterShow' : afterShow, 'afterHide': afterHide }; diff --git a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js index f78022877..6152927a2 100644 --- a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js +++ b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js @@ -10,10 +10,10 @@ var $dialog = null; function resetForm() { - // remove all display errors $('#recording-finished-dialog form .error-text').remove() $('#recording-finished-dialog form .error').removeClass("error") + removeGoogleLoginErrors() } function beforeShow() { @@ -108,6 +108,10 @@ context.jamClient.ClosePreviewRecording(); } + function onCancel() { + return false; + } + function discardRecording(e) { resetForm(); @@ -130,11 +134,47 @@ return false; } + function startGoogleLogin(e) { + e.preventDefault() + logger.debug("Starting google login") + window._oauth_win = window.open("/auth/google_login", "Log In to Google", "height=500,width=500,menubar=no,resizable=no,status=no"); + + window._oauth_callback = function() { + window._oauth_win.close() + setGoogleAuthState() + } + return false; + } + function claimRecording(e) { - resetForm(); - registerClaimRecordingHandlers(false); + registerClaimRecordingHandlers(false) + var upload_to_youtube = $('#recording-finished-dialog form input[name=upload_to_youtube]').is(':checked') + + if (upload_to_youtube) { + $.ajax({ + type: "GET", + dataType: "json", + url: "/auth/has_google_auth" + }).success(function(data) { + if(data.has_google_auth) { + performClaim() + } else { + var error_ul = $('
  • You must sign in to YouTube
') + $('#recording-finished-dialog form [purpose=upload_to_youtube]').addClass('error').append(error_ul) + } + }).always(function () { + registerClaimRecordingHandlers(true); + }) + } else { + performClaim() + } + + return false; + } + + function performClaim() { var name = $('#recording-finished-dialog form input[name=name]').val(); var description = $('#recording-finished-dialog form textarea[name=description]').val(); var genre = $('#recording-finished-dialog form select[name=genre]').val(); @@ -151,59 +191,53 @@ save_video: save_video, upload_to_youtube: upload_to_youtube }) - .done(function () { - $dialog.data('result', {keep:true}); - app.layout.closeDialog('recordingFinished'); - context.JK.GA.trackMakeRecording(); - }) - .fail(function (jqXHR) { - if (jqXHR.status == 422) { - var errors = JSON.parse(jqXHR.responseText); + .done(function () { + $dialog.data('result', {keep:true}); + app.layout.closeDialog('recordingFinished'); + context.JK.GA.trackMakeRecording(); + }) + .fail(function (jqXHR) { + if (jqXHR.status == 422) { + var errors = JSON.parse(jqXHR.responseText); - var $name_errors = context.JK.format_errors('name', errors); - if ($name_errors) $('#recording-finished-dialog form input[name=name]').closest('div.field').addClass('error').end().after($name_errors); + var $name_errors = context.JK.format_errors('name', errors); + if ($name_errors) $('#recording-finished-dialog form input[name=name]').closest('div.field').addClass('error').end().after($name_errors); - var $description_errors = context.JK.format_errors('description', errors); - if ($description_errors) $('#recording-finished-dialog form input[name=description]').closest('div.field').addClass('error').end().after($description_errors); + var $description_errors = context.JK.format_errors('description', errors); + if ($description_errors) $('#recording-finished-dialog form input[name=description]').closest('div.field').addClass('error').end().after($description_errors); - var $genre_errors = context.JK.format_errors('genre', errors); - if ($genre_errors) $('#recording-finished-dialog form select[name=genre]').closest('div.field').addClass('error').end().after($genre_errors); + var $genre_errors = context.JK.format_errors('genre', errors); + if ($genre_errors) $('#recording-finished-dialog form select[name=genre]').closest('div.field').addClass('error').end().after($genre_errors); - var $is_public_errors = context.JK.format_errors('is_public', errors); - if ($is_public_errors) $('#recording-finished-dialog form input[name=is_public]').closest('div.field').addClass('error').end().after($is_public_errors); + var $is_public_errors = context.JK.format_errors('is_public', errors); + if ($is_public_errors) $('#recording-finished-dialog form input[name=is_public]').closest('div.field').addClass('error').end().after($is_public_errors); - var $save_video_errors = context.JK.format_errors('save_video', errors); - if ($save_video_errors) $('#recording-finished-dialog form input[name=save_video]').closest('div.field').addClass('error').end().after($save_video_errors); + var $save_video_errors = context.JK.format_errors('save_video', errors); + if ($save_video_errors) $('#recording-finished-dialog form input[name=save_video]').closest('div.field').addClass('error').end().after($save_video_errors); + + var recording_error = context.JK.get_first_error('recording_id', errors); - var $upload_to_youtube_errors = context.JK.format_errors('upload_to_youtube', errors); - if ($upload_to_youtube_errors) $('#recording-finished-dialog form input[name=upload_to_youtube]').closest('div.field').addClass('error').end().after($upload_to_youtube_errors); + if (recording_error) context.JK.showErrorDialog(app, "Unable to claim recording.", recording_error); + } + else { + logger.error("unable to claim recording %o", arguments); - var recording_error = context.JK.get_first_error('recording_id', errors); - - if (recording_error) context.JK.showErrorDialog(app, "Unable to claim recording.", recording_error); - } - else { - logger.error("unable to claim recording %o", arguments); - - context.JK.showErrorDialog(app, "Unable to claim recording.", jqXHR.responseText); - } - }) - .always(function () { - registerClaimRecordingHandlers(true); - }); - return false; + context.JK.showErrorDialog(app, "Unable to claim recording.", jqXHR.responseText); + } + }) + .always(function () { + registerClaimRecordingHandlers(true); + }); } function registerClaimRecordingHandlers(onOff) { + $('#keep-session-recording').off('click', claimRecording) + $('#recording-finished-dialog form').off('submit', claimRecording); + if (onOff) { $('#keep-session-recording').on('click', claimRecording); $('#recording-finished-dialog form').on('submit', claimRecording); } - else { - $('#keep-session-recording').off('click', claimRecording) - $('#recording-finished-dialog form').off('submit', claimRecording); - } - } function registerDiscardRecordingHandlers(onOff) { @@ -216,6 +250,11 @@ } function onPause() { + logger.debug("calling jamClient.SessionPausePlay"); + context.jamClient.SessionPausePlay(); + } + + function onStop() { logger.debug("calling jamClient.SessionStopPlay"); context.jamClient.SessionStopPlay(); } @@ -234,9 +273,39 @@ registerClaimRecordingHandlers(true); registerDiscardRecordingHandlers(true); $(playbackControls) + .on('stop', onStop) .on('pause', onPause) .on('play', onPlay) .on('change-position', onChangePlayPosition); + $dialog.find(".google_login_button").on('click', startGoogleLogin); + + // Check for google authorization using AJAX and show/hide the + // google login button / "signed in" label as appropriate: + $(window).on('focus', function() { + setGoogleAuthState(); + }); + } + + function setGoogleAuthState() { + $.ajax({ + type: "GET", + dataType: "json", + url: "/auth/has_google_auth" + }).success(function(data) { + if(data.has_google_auth) { + $("input.google_login_button").addClass("hidden") + $("span.signed_in_to_google").removeClass("hidden") + removeGoogleLoginErrors() + } else { + $("span.signed_in_to_google").addClass("hidden") + $("input.google_login_button").removeClass("hidden") + } + }) + } + + function removeGoogleLoginErrors() { + $("ul.error-text.upload_to_youtube").remove() + $('#recording-finished-dialog form div[purpose=upload_to_youtube]').removeClass('error') } function setRecording(recordingData) { @@ -258,7 +327,8 @@ function initialize() { var dialogBindings = { 'beforeShow': beforeShow, - 'afterHide': afterHide + 'afterHide': afterHide, + 'onCancel': onCancel }; app.bindDialog('recordingFinished', dialogBindings); @@ -267,7 +337,6 @@ playbackControls = new context.JK.PlaybackControls($('#recording-finished-dialog .recording-controls')); registerStaticEvents(); - initializeButtons(); }; diff --git a/web/app/assets/javascripts/dialog/sessionSettingsDialog.js b/web/app/assets/javascripts/dialog/sessionSettingsDialog.js index ea18fcd01..0c47eb20e 100644 --- a/web/app/assets/javascripts/dialog/sessionSettingsDialog.js +++ b/web/app/assets/javascripts/dialog/sessionSettingsDialog.js @@ -3,6 +3,7 @@ context.JK = context.JK || {}; context.JK.SessionSettingsDialog = function(app, sessionScreen) { var logger = context.JK.logger; + var gearUtils = context.JK.GearUtilsInstance; var $dialog; var $screen = $('#session-settings'); var $selectedFilenames = $screen.find('#selected-filenames'); @@ -15,6 +16,8 @@ function beforeShow(data) { + var canPlayWithOthers = gearUtils.canPlayWithOthers(); + context.JK.GenreSelectorHelper.render('#session-settings-genre'); $dialog = $('[layout-id="session-settings"]'); @@ -72,6 +75,10 @@ context.JK.dropdown($('#session-settings-language')); context.JK.dropdown($('#session-settings-musician-access')); context.JK.dropdown($('#session-settings-fan-access')); + + var easyDropDownState = canPlayWithOthers.canPlay ? 'enable' : 'disable' + $('#session-settings-musician-access').easyDropDown(easyDropDownState) + $('#session-settings-fan-access').easyDropDown(easyDropDownState) } function saveSettings(evt) { diff --git a/web/app/assets/javascripts/dialog/signinDialog.js b/web/app/assets/javascripts/dialog/signinDialog.js index a76d649d5..a1b56b11d 100644 --- a/web/app/assets/javascripts/dialog/signinDialog.js +++ b/web/app/assets/javascripts/dialog/signinDialog.js @@ -11,11 +11,21 @@ var dialogId = '#signin-dialog'; var $dialog = null; var signinHelper = null; + var redirectTo = null; - function beforeShow() { + function beforeShow(args) { logger.debug("showing login form") - signinHelper.reset(); + if(args.redirect_to) { + redirectTo = "/client#/redeemComplete" + } + else { + redirectTo = null; + } + if(redirectTo) { + logger.debug("setting redirect to in login dialog") + } + signinHelper.reset(redirectTo); } function afterShow() { @@ -24,6 +34,7 @@ function afterHide() { logger.debug("hiding login form") + redirectTo = null; } function initialize(){ diff --git a/web/app/assets/javascripts/dialog/singlePlayerProfileGuard.js.coffee b/web/app/assets/javascripts/dialog/singlePlayerProfileGuard.js.coffee new file mode 100644 index 000000000..4f9375b7e --- /dev/null +++ b/web/app/assets/javascripts/dialog/singlePlayerProfileGuard.js.coffee @@ -0,0 +1,81 @@ +$ = jQuery +context = window +context.JK ||= {} + +context.JK.SinglePlayerProfileGuardDialog = class SinglePlayerProfileGuardDialog + constructor: (@app) -> + @rest = context.JK.Rest() + @client = context.jamClient + @logger = context.JK.logger + @gearUtils = context.JK.GearUtilsInstance + @screen = null + @dialogId = 'single-player-profile-dialog'; + @dialog = null; + + initialize:() => + dialogBindings = { + 'beforeShow' : @beforeShow, + 'afterShow' : @afterShow + } + + @dialog = $('[layout-id="' + @dialogId + '"]'); + @app.bindDialog(@dialogId, dialogBindings); + @content = @dialog.find(".dialog-inner") + @audioLatency = @dialog.find('.audio-latency') + @btnPrivateSession = @dialog.find('.btn-private-session') + @btnGearSetup = @dialog.find('.btn-gear-setup') + @btnRescan = @dialog.find('.btn-rescan') + @rescanningNotice = @dialog.find('.rescanning-notice') + + @btnPrivateSession.on('click', @onPrivateSessionChoice) + @btnGearSetup.on('click', @onGearSetupChoice) + @btnRescan.on('click', @onRescan) + + beforeShow:() => + @dialog.data('result', { choice: null}) + + + onRescan: () => + @gearUtils.scheduleAudioRestart('single-player-guard', 1000, @beforeScan, @afterScan) + + beforeScan: () => + @dialog.find('.action-buttons a').addClass('disabled') + @rescanningNotice.show(); + + afterScan: (canceled) => + @dialog.find('.action-buttons a').removeClass('disabled') + @rescanningNotice.hide(); + + if !canceled + result = context.jamClient.ReloadAudioSystem(true, true, true); + + @refresh(); + + refresh:() => + canPlayWithOthers = @gearUtils.canPlayWithOthers() + + if canPlayWithOthers.isNoInputProfile + @content.removeClass('high-latency').addClass('has-no-inputs') + else + @content.removeClass('has-no-input').addClass('high-latency') + + latency = '?' + if canPlayWithOthers.audioLatency? + latency = canPlayWithOthers.audioLatency.toFixed(2) + + @audioLatency.text("#{latency} milliseconds.") + + afterShow:() => + @refresh() + + + + onPrivateSessionChoice: () => + @dialog.data('result', { choice: 'private_session'}) + @app.layout.closeDialog(@dialogId) + return false + + onGearSetupChoice: () => + @dialog.data('result', { choice: 'gear_setup'}) + @app.layout.closeDialog(@dialogId) + return false \ No newline at end of file diff --git a/web/app/assets/javascripts/dialog/videoDialog.js b/web/app/assets/javascripts/dialog/videoDialog.js index 3d5a41707..01ab3dc6b 100644 --- a/web/app/assets/javascripts/dialog/videoDialog.js +++ b/web/app/assets/javascripts/dialog/videoDialog.js @@ -14,7 +14,7 @@ if (!context.jamClient || !context.jamClient.IsNativeClient()) { $('#video-dialog-header').html($self.data('video-header') || $self.attr('data-video-header')); - $('#video-dialog-iframe').attr('src', $self.data('video-url') || $self.atr('data-video-url')); + $('#video-dialog-iframe').attr('src', $self.data('video-url') || $self.attr('data-video-url')); app.layout.showDialog('video-dialog'); e.stopPropagation(); e.preventDefault(); @@ -29,6 +29,7 @@ function events() { $('.carousel .slides').on('click', '.slideItem', videoClick); $('.video-slide').on('click', videoClick); + $('.video-item').on('click', videoClick); $(dialogId + '-close').click(function (e) { app.layout.closeDialog('video-dialog'); diff --git a/web/app/assets/javascripts/download_jamtrack.js.coffee b/web/app/assets/javascripts/download_jamtrack.js.coffee index 8442fec02..58592a73c 100644 --- a/web/app/assets/javascripts/download_jamtrack.js.coffee +++ b/web/app/assets/javascripts/download_jamtrack.js.coffee @@ -48,6 +48,8 @@ context.JK.DownloadJamTrack = class DownloadJamTrack @tracked = false @ajaxEnqueueAborted = false @ajaxGetJamTrackRightAborted = false + @currentPackagingStep = null + @totalSteps = null throw "no JamTrack specified" unless @jamTrack? throw "invalid size" if @size != 'large' && @size != 'small' @@ -174,6 +176,7 @@ context.JK.DownloadJamTrack = class DownloadJamTrack if @state == @states.errored data.result = 'error' data.detail = @errorReason + @rest.createAlert("JamTrack Sync failed for #{context.JK.currentUserName}", data) else data.result = 'success' @@ -187,7 +190,10 @@ context.JK.DownloadJamTrack = class DownloadJamTrack showDownloading: () => @logger.debug("showing #{@state.name}") # while downloading, we don't run the transition timer, because the download API is guaranteed to call success, or failure, eventually - context.jamClient.JamTrackDownload(@jamTrack.id, this.makeDownloadSuccessCallback(), this.makeDownloadFailureCallback()) + context.jamClient.JamTrackDownload(@jamTrack.id, context.JK.currentUserId, + this.makeDownloadProgressCallback(), + this.makeDownloadSuccessCallback(), + this.makeDownloadFailureCallback()) showKeying: () => @logger.debug("showing #{@state.name}") @@ -199,6 +205,10 @@ context.JK.DownloadJamTrack = class DownloadJamTrack showInitial: () => @logger.debug("showing #{@state.name}") + @sampleRate = context.jamClient.GetSampleRate() + @fingerprint = context.jamClient.SessionGetMacHash() + logger.debug("fingerprint: ", @fingerprint) + @sampleRateForFilename = if @sampleRate == 48 then '48' else '44' @attempts = @attempts + 1 this.expectTransition() context.JK.SubscriptionUtils.subscribe('jam_track_right', @jamTrack.jam_track_right_id).on(context.JK.EVENTS.SUBSCRIBE_NOTIFICATION, this.onJamTrackRightEvent) @@ -344,17 +354,18 @@ context.JK.DownloadJamTrack = class DownloadJamTrack checkState: () => # check for the success state against the local state of the client... if it's playable, then we should be OK - @trackDetail = context.jamClient.JamTrackGetTrackDetail (@jamTrack.id) + fqId = "#{@jamTrack.id}-#{@sampleRateForFilename}" + @trackDetail = context.jamClient.JamTrackGetTrackDetail (fqId) - @logger.debug("DownloadJamTrack: JamTrackGetTrackDetail.key_state: " + @trackDetail.key_state) + @logger.debug("DownloadJamTrack: JamTrackGetTrackDetail(#{fqId}).key_state: " + @trackDetail.key_state, @trackDetail) # first check if the version is not the same; if so, invalidate. if @trackDetail.version? if @jamTrack.version != @trackDetail.version @logger.info("DownloadJamTrack: JamTrack on disk is different version (stored: #{@trackDetail.version}, server: #{@jamTrack.version}. Invalidating") - context.jamClient.InvalidateJamTrack(@jamTrack.id) - @trackDetail = context.jamClient.JamTrackGetTrackDetail (@jamTrack.id) + context.jamClient.InvalidateJamTrack("#{@jamTrack.id}-#{@sampleRateForFilename}") + @trackDetail = context.jamClient.JamTrackGetTrackDetail ("#{@jamTrack.id}-#{@sampleRateForFilename}") if @trackDetail.version? @logger.error("after invalidating package, the version is still wrong!") @@ -374,8 +385,22 @@ context.JK.DownloadJamTrack = class DownloadJamTrack .done(this.processJamTrackRight) .fail(this.processJamTrackRightFail) + # update progress indicator for packaging step + updateSteps: () => + if @currentPackagingStep? and @totalSteps? + progress = "#{Math.round(@currentPackagingStep/@totalSteps * 100)}%" + else + progress = '...' + + @root.find('.state-packaging .progress').text(progress) + + processSigningState: (jamTrackRight) => + signingState = jamTrackRight.signing_state + @totalSteps = jamTrackRight.packaging_steps + @currentPackagingStep = jamTrackRight.current_packaging_step + + @updateSteps() - processSigningState: (signingState) => @logger.debug("DownloadJamTrack: processSigningState: " + signingState) switch signingState @@ -427,16 +452,15 @@ context.JK.DownloadJamTrack = class DownloadJamTrack @attemptedEnqueue = true @ajaxEnqueueAborted = false - sampleRate = context.jamClient.GetSampleRate() - - @rest.enqueueJamTrack({id: @jamTrack.id, sample_rate: sampleRate}) + @rest.enqueueJamTrack({id: @jamTrack.id, sample_rate: @sampleRate, fingerprint: @fingerprint}) .done(this.processEnqueueJamTrack) .fail(this.processEnqueueJamTrackFail) processJamTrackRight: (myJamTrack) => + @logger.debug("processJamTrackRight", myJamTrack) unless @ajaxGetJamTrackRightAborted - this.processSigningState(myJamTrack.signing_state) + this.processSigningState(myJamTrack) else @logger.debug("DownloadJamTrack: ignoring processJamTrackRight response") @@ -452,23 +476,50 @@ context.JK.DownloadJamTrack = class DownloadJamTrack else @logger.debug("DownloadJamTrack: ignoring processEnqueueJamTrack response") - processEnqueueJamTrackFail: () => + displayUIForGuard:(response) => + display = switch response.message + when 'no user specified' then 'Please log back in.' + when 'no fingerprint specified' then 'There was a problem communicating between client and server. Please restart JamKazam.' + when 'no all fingerprint specified' then 'There was a problem communicating between client and server. Please restart JamKazam.' + when 'no running fingerprint specified' then 'There was a problem communicating between client and server. Please restart JamKazam.' + when 'already redeemed another' then "It appears you have already redeemed your one free JamTrack for your household. We are sorry, but we cannot let you download this JamTrack free. If you believe this is an error, please contact us at support@jamkazam.com." + when "other user has 'all' fingerprint" then "It appears you have already redeemed your one free JamTrack for your household. We are sorry, but we cannot let you download this JamTrack free. If you believe this is an error, please contact us at support@jamkazam.com." + when "other user has 'running' fingerprint" then "It appears you have already redeemed your one free JamTrack for your household. We are sorry, but we cannot let you download this JamTrack free. If you believe this is an error, please contact us at support@jamkazam.com." + else "Something went wrong #{response.message}. Please restart JamKazam" + + processEnqueueJamTrackFail: (jqXHR) => unless @ajaxEnqueueAborted - this.transitionError("enqueue-error", "Unable to ask the server to build your JamTrack.") + if jqXHR.status == 403 + display = this.displayUIForGuard(JSON.parse(jqXHR.responseText)) + this.transitionError("enqueue-error", display) + else + this.transitionError("enqueue-error", "Unable to ask the server to build your JamTrack.") else @logger.debug("DownloadJamTrack: ignoring processEnqueueJamTrackFail response") onJamTrackRightEvent: (e, data) => - @logger.debug("DownloadJamTrack: subscription notification received: type:" + data.type) + @logger.debug("DownloadJamTrack: subscription notification received: type:" + data.type, data) this.expectTransition() - this.processSigningState(data.body.signing_state) + this.processSigningState(data.body) - downloadProgressCallback: (bytesReceived, bytesTotal, downloadSpeedMegSec, timeRemaining) => - bytesReceived = Number(bytesReceived) - bytesTotal = Number(bytesTotal) - # bytesTotal from Qt is not trust worthy; trust server's answer instead - #progressWidth = ((bytesReceived / updateSize) * 100).toString() + "%"; - # $('#progress-bar').width(progressWidth) + updateDownloadProgress: () => + + if @bytesReceived? and @bytesTotal? + progress = "#{Math.round(@bytesReceived/@bytesTotal * 100)}%" + else + progress = '0%' + + @root.find('.state-downloading .progress').text(progress) + + downloadProgressCallback: (bytesReceived, bytesTotal) => + @logger.debug("download #{bytesReceived}/#{bytesTotal}") + + @bytesReceived = Number(bytesReceived) + @bytesTotal = Number(bytesTotal) + + # the reason this timeout is set is because, without it, + # we observe that the client will hang. So, if you remove this timeout, make sure to test with real client + setTimeout(this.updateDownloadProgress, 100) downloadSuccessCallback: (updateLocation) => # is the package loadable yet? diff --git a/web/app/assets/javascripts/everywhere/everywhere.js b/web/app/assets/javascripts/everywhere/everywhere.js index 16adb8b3f..43a844a32 100644 --- a/web/app/assets/javascripts/everywhere/everywhere.js +++ b/web/app/assets/javascripts/everywhere/everywhere.js @@ -20,6 +20,12 @@ var stun = null; var rest = context.JK.Rest(); + if(gon.global.web_performance_timing_enabled) { + $(window).on('load', sendTimingResults) + $(window).on('pagehide', setNavigationStart) + } + + $(document).on('JAMKAZAM_CONSTRUCTED', function(e, data) { var app = data.app; @@ -31,6 +37,8 @@ updateScoringIntervals(); initializeInfluxDB(); + + trackNewUser(); }) $(document).on('JAMKAZAM_READY', function() { @@ -204,10 +212,10 @@ var user = app.user() if(user) { user.done(function(userProfile) { - console.log("app.layout.getCurrentScreen() != 'checkoutOrderScreen'", app.layout.getCurrentScreen()) - if (userProfile.show_whats_next && + if (!userProfile.show_jamtrack_guide && userProfile.show_whats_next && userProfile.show_whats_next_count < 10 && window.location.pathname.indexOf(gon.client_path) == 0 && - window.location.pathname.indexOf('/checkout') == -1 && + window.location.hash.indexOf('/checkout') == -1 && + window.location.hash.indexOf('/redeem') == -1 && !app.layout.isDialogShowing('getting-started')) { app.layout.showDialog('getting-started'); @@ -220,4 +228,85 @@ context.JK.JamTrackUtils.checkShoppingCart(); } + function trackNewUser() { + var cookie = $.cookie('new_user') + + if(cookie) { + try { + cookie = JSON.parse(cookie) + + context.JK.signupData = {} + context.JK.signupData = cookie + + $(function() { + // ga() object isn't ready until the page is loaded + $.removeCookie('new_user') + context.JK.GA.trackRegister(cookie.musician, cookie.registrationType); + }); + } + catch(e) { + logger.error("unable to deserialize new_user cookie") + } + } + } + + function setNavigationStart() { + if(!window.performance || !window.performance.timing) { + try { + window.sessionStorage.setItem('navigationStart', Date.now()) + } + catch(e) { + logger.debug("unable to accesss sessionStorage") + } + } + + } + + + // http://githubengineering.com/browser-monitoring-for-github-com/ + function sendTimingResults() { + + setTimeout(function() { + var timing; + var hasTimingApi; + + if(window.performance && window.performance.timing) { + timing = window.performance.timing + hasTimingApi = true; + } + else { + timing = {} + hasTimingApi = false; + } + + // Merge in simulated cross-browser load event + timing['crossBrowserLoadEvent'] = Date.now() + + var chromeFirstPaintTime; + if(window.chrome && window.chrome.loadTimes) { + var loadTimes = window.chrome.loadTimes() + if(loadTimes) { + chromeFirstPaintTime = true; + } + } + + // firstPaintTime is in seconds; convert to milliseconds to match performance.timing. + timing['chromeFirstPaintTime'] = Math.round(chromeFirstPaintTime * 1000); + + // Merge in simulated navigation start, if no navigation timing + if (!hasTimingApi) { + try { + var navStart = window.sessionStorage.getItem('navigationStart') + if(navStart) { + timing['navigationStart'] = parseInt(navStart, 10) + } + } + catch(e) { } + } + + context.JK.GA.trackTiming(timing); + }, 0) + + } + })(window, jQuery); diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index d0b640b8d..e571976b2 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -21,12 +21,14 @@ var frameSize = 2.5; var fakeJamClientRecordings = null; var p2pCallbacks = null; + var videoShared = false; var metronomeActive=false; var metronomeBPM=false; var metronomeSound=false; var metronomeMeter=0; - var backingTrackPath=""; - var backingTrackLoop=false; + var backingTrackPath = ""; + var backingTrackLoop = false; + var simulateNoInputs = false; function dbg(msg) { logger.debug('FakeJamClient: ' + msg); } @@ -47,11 +49,67 @@ function FTUEPageLeave() {} function FTUECancel() {} function FTUEGetMusicProfileName() { - return "FTUEAttempt-1" + + if(simulateNoInputs) { + return "System Default (Playback Only)" + } + else { + return "FTUEAttempt-1" + } } function FTUESetMusicProfileName() { } + + function FTUESetPreferredMixerSampleRate() {} + function FTUESetPreferredOutputSampleRate(){ } + function FTUESetPreferredChatSampleRate() {} + function FTUEgetInputDeviceSampleRate() { + return 44100; + } + function FTUEgetOutputDeviceSampleRate() { + return 44100; + } + + + function FTUESelectVideoCaptureDevice(device, settings) { + + } + function FTUESetVideoEncodeResolution(resolution) { + + } + function FTUEGetVideoCaptureDeviceNames() { + return ["Built-in Webcam HD"] + } + function FTUECurrentSelectedVideoDevice() { + return {"xy323ss": "Built-in Webcam HD"} + } + function FTUEGetAvailableEncodeVideoResolutions() { + return { + 1: "1024x768", + 2: "800x600" + } + } + + function FTUEGetVideoCaptureDeviceCapabilities() { + return {} + } + + function isSessVideoShared() { + return videoShared; + } + + function SessStopVideoSharing() { + videoShared=false; + } + + function SessStartVideoSharing(bitrate) { + if (!bitrate || typeof(bitrate)=="undefined") { + bitrate = 0 + } + videoShared=true; + } + function FTUEGetInputLatency() { dbg("FTUEGetInputLatency"); return 2; @@ -266,6 +324,10 @@ return false; } + function FTUECreateUpdatePlayBackProfile() { + return true; + } + function RegisterVolChangeCallBack(functionName) { dbg('RegisterVolChangeCallBack'); } @@ -392,6 +454,14 @@ } // Session Functions + + function SessionCurrrentJamTrackPlayPosMs() { + return 0; + } + + function SessionGetJamTracksPlayDurationMs() { + return 60000; + } function SessionAddTrack() {} function SessionAudioResync() { @@ -444,6 +514,10 @@ ]; var response = []; for (var i=0; i 1, + inSession); + }// if + })// rest.getJamTracks + }// if + })// rest.getSession + } + + function trackJamTrackPlay(isGlobal, isPublic, inSession) { + assertBoolean(isGlobal) + assertBoolean(isPublic) + assertBoolean(inSession) + context.ga( + 'send', + 'event', + (isGlobal) ? jamTrackAvailabilityTypes.worldwide : jamTrackAvailabilityTypes.usa, + (isPublic) ? jamTrackActions.isPublic : jamTrackActions.isPrivate, + (inSession) ? jamTrackSessionLabels.inSession : jamTrackSessionLabels.nonSession + ) + logger.debug("Tracked Jam Track Play") + } + + function trackTiming(timing) { + + if(!timing) {return} + + try { + var computed = { + dns: timing.domainLookupEnd - timing.domainLookupStart, + connect: timing.connectEnd - timing.connectStart, + ttfb: timing.responseStart - timing.connectEnd, + basePage: timing.responseEnd - timing.responseStart, + frontEnd: timing.loadEventStart - timing.responseEnd, + domContentLoadedEvent: timing.domContentLoadedEventEnd - timing.domContentLoadedEventStart, + windowLoadEvent: timing.loadEventEnd - timing.loadEventStart, + domInteractive: timing.domInteractive - timing.domLoading, + domComplete: timing.domComplete - timing.domLoading, + domCompleteToOnload: timing.loadEventStart - timing.domComplete + }; + + logger.debug("page load time: " + computed.frontEnd) + context._.each(computed, function (value, key) { + if (value > 0 && value < 60000) { + context.ga("send", "timing", "NavigationTiming", key, value, null, {'page' : '/' + window.location.pathname}); + } + }) + //context.stats.write('web.timing.navigation', computed) + } + catch(e) { + logger.error("loading times failed in ga.js", e) + } + } + // if you want to pass in no title, either omit it from the arg list when u invoke virtualPageView, or pass in undefined, NOT null function virtualPageView(page, title) { @@ -271,7 +347,8 @@ } // when someone plays a recording - function trackRecordingPlay(recordingAction) { + function trackRecordingPlay(recording, recordingAction) { + if (!recordingAction) { recordingAction = _defaultPlayAction(); } @@ -279,10 +356,20 @@ var label = JK.currentUserId ? userLabels.registeredUser : userLabels.visitor; context.ga('send', 'event', categories.recordingPlay, recordingAction, label); + + if (recording.jam_track) { + rest.getJamTracks({id:recording.jam_track_id}).done(function(response) { + if (response.jamtracks && response.jamtracks.length==1) { + var jamtrack = response.jamtracks[0] + trackJamTrackPlay(jamtrack.sales_region!=context.JK.AVAILABILITY_US, recording.fan_access, false); + } + }) + } } // when someone plays a live session broadcast - function trackSessionPlay(recordingAction) { + function trackSessionPlay(session, recordingAction) { + logger.debug("Tracking session play: ", session) if (!recordingAction) { recordingAction = _defaultPlayAction(); } @@ -379,7 +466,8 @@ GA.trackSessionQuality = trackSessionQuality; GA.trackServiceInvitations = trackServiceInvitations; GA.trackFindSessions = trackFindSessions; - GA.virtualPageView = virtualPageView; + GA.trackJamTrackPlay = trackJamTrackPlay; + GA.trackJamTrackPlaySession = trackJamTrackPlaySession; GA.trackFriendConnect = trackFriendConnect; GA.trackMakeRecording = trackMakeRecording; GA.trackShareRecording = trackShareRecording; @@ -387,8 +475,9 @@ GA.trackSessionPlay = trackSessionPlay; GA.trackBand = trackBand; GA.trackJKSocial = trackJKSocial; - - + GA.virtualPageView = virtualPageView; + GA.trackTiming = trackTiming; + context.JK.GA = GA; })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index 175a96fd6..85ebcc250 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -52,7 +52,8 @@ JAMTRACK_DOWNLOADER_STATE_CHANGED: 'jamtrack_downloader_state', METRONOME_PLAYBACK_MODE_SELECTED: 'metronome_playback_mode_selected', CHECKOUT_SIGNED_IN: 'checkout_signed_in', - CHECKOUT_SKIP_SIGN_IN: 'checkout_skip_sign_in' + CHECKOUT_SKIP_SIGN_IN: 'checkout_skip_sign_in', + PREVIEW_PLAYED: 'preview_played' }; context.JK.PLAYBACK_MONITOR_MODE = { diff --git a/web/app/assets/javascripts/helpBubbleHelper.js b/web/app/assets/javascripts/helpBubbleHelper.js index 9c9a05bd5..c8ccdc4cf 100644 --- a/web/app/assets/javascripts/helpBubbleHelper.js +++ b/web/app/assets/javascripts/helpBubbleHelper.js @@ -10,6 +10,7 @@ var rest = new context.JK.Rest(); context.JK.HelpBubbleHelper = helpBubble; var logger = context.JK.logger; + var jamTrackGuideTimeout = null; var defaultScoreBreakDownOptions = {positions: ['right', 'top', 'bottom', 'left'], width:'600px', closeWhenOthersOpen: true }; helpBubble.scoreBreakdown = function($element, isCurrentUser, full_score, myAudioLatency, otherAudioLatency, internetScore, options) { @@ -26,4 +27,106 @@ } } + function bigHelpOptions(options) { + return {positions: options.positions, offsetParent: options.offsetParent, + spikeGirth: 15, + spikeLength: 20, + fill: 'white', + cornerRadius:8, + strokeWidth: 2, + cssStyles: { + fontWeight:'bold', + fontSize: '20px', + backgroundColor:'transparent', + color:'#ed3618'}} + } + + function clearJamTrackGuideTimeout() { + if(jamTrackGuideTimeout) { + clearTimeout(jamTrackGuideTimeout); + jamTrackGuideTimeout = null; + } + } + + function jamTrackGuide(callback) { + if(gon.isNativeClient) { + context.JK.app.user().done(function(user) { + if(user.show_jamtrack_guide) { + clearJamTrackGuideTimeout(); + jamTrackGuideTimeout = setTimeout(function() { + callback() + }, 1000) + + } + }) + } + } + + helpBubble.rotateJamTrackLandingBubbles = function($preview, $video, $ctaButton, $alternativeCta) { + $(window).on('load', function() { + setTimeout(function() { + helpBubble.jamtrackLandingPreview($preview, $preview.offsetParent()) + + setTimeout(function() { + helpBubble.jamtrackLandingVideo($video, $video.offsetParent()) + + setTimeout(function() { + helpBubble.jamtrackLandingCta($ctaButton, $alternativeCta) + }, 11000); // 5 seconds on top of 6 second show time of bubbles + }, 11000); // 5 seconds on top of 6 second show time of bubbles + }, 1500) + + + }) + } + + helpBubble.clearJamTrackGuide = clearJamTrackGuideTimeout; + + helpBubble.jamtrackGuideTile = function ($element, $offsetParent) { + jamTrackGuide(function() { + context.JK.prodBubble($element, 'jamtrack-guide-tile', {}, bigHelpOptions({positions:['top'], offsetParent: $offsetParent})) + }) + } + + helpBubble.jamtrackGuidePrivate = function ($element, $offsetParent) { + jamTrackGuide(function() { + context.JK.prodBubble($element, 'jamtrack-guide-private', {}, bigHelpOptions({positions:['right'], offsetParent: $offsetParent})) + }) + } + + helpBubble.jamtrackGuideSession = function ($element, $offsetParent) { + jamTrackGuide(function() { + context.JK.prodBubble($element, 'jamtrack-guide-session', {}, bigHelpOptions({positions:['left'], offsetParent: $offsetParent})) + }) + } + + helpBubble.jamtrackLandingPreview = function($element, $offsetParent) { + context.JK.prodBubble($element, 'jamtrack-landing-preview', {}, bigHelpOptions({positions:['right'], offsetParent: $offsetParent})) + } + + helpBubble.jamtrackLandingVideo = function($element, $offsetParent) { + context.JK.prodBubble($element, 'jamtrack-landing-video', {}, bigHelpOptions({positions:['left'], offsetParent: $offsetParent})) + } + + helpBubble.jamtrackLandingCta = function($element, $alternativeElement) { + if ($element.visible()) { + context.JK.prodBubble($element, 'jamtrack-landing-cta', {}, bigHelpOptions({positions:['left']})) + } + else { + context.JK.prodBubble($alternativeElement, 'jamtrack-landing-cta', {}, bigHelpOptions({positions:['right']})) + } + } + + helpBubble.jamtrackBrowseBand = function($element, $offsetParent) { + return context.JK.prodBubble($element, 'jamtrack-browse-band', {}, bigHelpOptions({positions:['top'], offsetParent: $offsetParent})) + } + + helpBubble.jamtrackBrowseMasterMix = function($element, $offsetParent) { + return context.JK.prodBubble($element, 'jamtrack-browse-master-mix', {}, bigHelpOptions({positions:['top'], offsetParent: $offsetParent})) + } + + helpBubble.jamtrackBrowseCta = function($element, $offsetParent) { + return context.JK.prodBubble($element, 'jamtrack-browse-cta', {}, bigHelpOptions({positions:['top'], offsetParent: $offsetParent})) + } + })(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/homeScreen.js b/web/app/assets/javascripts/homeScreen.js index ec7b86d50..a96075b47 100644 --- a/web/app/assets/javascripts/homeScreen.js +++ b/web/app/assets/javascripts/homeScreen.js @@ -11,6 +11,14 @@ function beforeShow(data) { } + function afterShow(data) { + context.JK.HelpBubbleHelper.jamtrackGuideTile($('.homecard.createsession'), $screen.find('.createsession')); + } + + function beforeHide() { + context.JK.HelpBubbleHelper.clearJamTrackGuide(); + } + function mouseenterTile() { $(this).addClass('hover'); } @@ -84,7 +92,7 @@ } this.initialize = function() { - var screenBindings = { 'beforeShow': beforeShow }; + var screenBindings = { 'beforeShow': beforeShow, 'afterShow': afterShow, 'beforeHide' : beforeHide }; app.bindScreen('home', screenBindings); events(); $screen = $('.screen[layout-id="home"]') diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 0c2219309..a44e3b13d 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -295,11 +295,6 @@ } function addPlayablePlay(playableId, playableType, claimedRecordingId, userId) { - if (playableType == 'JamRuby::Recording') { - context.JK.GA.trackRecordingPlay(); - } else if (playableType == 'JamRuby::MusicSession') { - context.JK.GA.trackSessionPlay(); - } return $.ajax({ url: '/api/users/' + playableId + "/plays", type: "POST", @@ -498,6 +493,80 @@ return detail; } + function createAffiliatePartner(options) { + return $.ajax({ + type: "POST", + url: '/api/affiliate_partners', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + function getAffiliatePartnerData(userId) { + return $.ajax({ + type: "GET", + dataType: "json", + url: "/api/users/"+userId+"/affiliate_partner" + }); + } + + function postAffiliatePartnerData(userId, data) { + return $.ajax({ + type: "POST", + dataType: "json", + url: "/api/users/"+userId+"/affiliate_partner", + contentType: 'application/json', + processData:false, + data: JSON.stringify(data) + }); + } + + function getLinks(type, partner_id) { + var url = "/api/links/" + type; + + if(partner_id) { + url += '?affiliate_id=' + partner_id; + } + return $.ajax({ + type: "GET", + dataType: "json", + url: url + }); + } + + function getAffiliateSignups() { + return $.ajax({ + type: "GET", + dataType: "json", + url: "/api/affiliate_partners/signups" + }); + } + + function getAffiliateMonthly() { + return $.ajax({ + type: "GET", + dataType: "json", + url: "/api/affiliate_partners/monthly_earnings" + }); + } + + function getAffiliateQuarterly() { + return $.ajax({ + type: "GET", + dataType: "json", + url: "/api/affiliate_partners/quarterly_earnings" + }); + } + + function getAffiliatePayments() { + return $.ajax({ + type: "GET", + dataType: "json", + url: "/api/affiliate_partners/payments" + }); + } + function getCities(options) { var country = options['country'] var region = options['region'] @@ -1460,7 +1529,7 @@ }); } - function getJamtracks(options) { + function getJamTracks(options) { return $.ajax({ type: "GET", url: '/api/jamtracks?' + $.param(options), @@ -1469,6 +1538,15 @@ }); } + function getJamTrackArtists(options) { + return $.ajax({ + type: "GET", + url: '/api/jamtracks/artists?' + $.param(options), + dataType: "json", + contentType: 'application/json' + }); + } + function getJamTrackRight(options) { var jamTrackId = options['id']; @@ -1482,12 +1560,13 @@ function enqueueJamTrack(options) { var jamTrackId = options['id']; + delete options['id'] return $.ajax({ type: "POST", - url: '/api/jamtracks/enqueue/' + jamTrackId + '?' + $.param(options), + url: '/api/jamtracks/enqueue/' + jamTrackId, dataType: "json", - contentType: 'applications/json' + data: options }); } @@ -1507,9 +1586,18 @@ dataType: "json", contentType: 'application/json' }); - } + } - function getBackingTracks(options) { + function getSalesHistory(options) { + return $.ajax({ + type: "GET", + url: '/api/payment_histories?' + $.param(options), + dataType: "json", + contentType: 'application/json' + }); + } + + function getBackingTracks(options) { return $.ajax({ type: "GET", url: '/api/backing_tracks?' + $.param(options), @@ -1647,7 +1735,50 @@ }); } - function initialize() { + function createSignupHint(data) { + return $.ajax({ + type: "POST", + url: '/api/signup_hints', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(data), + }); + } + + function signup(data) { + return $.ajax({ + type: "POST", + url: '/api/users', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(data), + }); + } + + function portOverCarts() { + return $.ajax({ + type: "POST", + url: '/api/shopping_carts/port', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(data) + }) + } + + function createAlert(subject, data) { + var message = {subject:subject}; + $.extend(message, data); + console.log("message", message) + return $.ajax({ + type: "POST", + url: '/api/alerts', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(message), + }); + } + + function initialize() { return self; } @@ -1663,6 +1794,14 @@ this.cancelSession = cancelSession; this.updateScheduledSession = updateScheduledSession; this.getUserDetail = getUserDetail; + this.getAffiliatePartnerData = getAffiliatePartnerData; + this.postAffiliatePartnerData = postAffiliatePartnerData; + this.createAffiliatePartner = createAffiliatePartner; + this.getLinks = getLinks; + this.getAffiliateSignups = getAffiliateSignups; + this.getAffiliateMonthly = getAffiliateMonthly; + this.getAffiliateQuarterly = getAffiliateQuarterly; + this.getAffiliatePayments = getAffiliatePayments; this.getCities = getCities; this.getRegions = getRegions; this.getCountries = getCountries; @@ -1771,9 +1910,11 @@ this.updateAudioLatency = updateAudioLatency; this.getJamTrack = getJamTrack; this.getJamTrackWithArtistInfo = getJamTrackWithArtistInfo; - this.getJamtracks = getJamtracks; + this.getJamTracks = getJamTracks; + this.getJamTrackArtists = getJamTrackArtists; this.getPurchasedJamTracks = getPurchasedJamTracks; this.getPaymentHistory = getPaymentHistory; + this.getSalesHistory = getSalesHistory; this.getJamTrackRight = getJamTrackRight; this.enqueueJamTrack = enqueueJamTrack; this.getBackingTracks = getBackingTracks; @@ -1794,9 +1935,13 @@ this.markRecordedBackingTrackSilent = markRecordedBackingTrackSilent; this.addRecordingTimeline = addRecordingTimeline; this.playJamTrack = playJamTrack; + this.createSignupHint = createSignupHint; + this.createAlert = createAlert; + this.signup = signup; + this.portOverCarts = portOverCarts; return this; }; -})(window,jQuery); \ No newline at end of file +})(window,jQuery); diff --git a/web/app/assets/javascripts/jam_track_preview.js.coffee b/web/app/assets/javascripts/jam_track_preview.js.coffee index 926a8d824..f78a25d05 100644 --- a/web/app/assets/javascripts/jam_track_preview.js.coffee +++ b/web/app/assets/javascripts/jam_track_preview.js.coffee @@ -9,7 +9,7 @@ context.JK.JamTrackPreview = class JamTrackPreview @EVENTS = context.JK.EVENTS @rest = context.JK.Rest() @logger = context.JK.logger - @options = options || {master_shows_duration: false} + @options = options || {master_shows_duration: false, color:'gray', add_line_break: false, preload_master:false} @app = app @jamTrack = jamTrack @jamTrackTrack = jamTrackTrack @@ -19,16 +19,21 @@ context.JK.JamTrackPreview = class JamTrackPreview @instrumentIcon = null @instrumentName = null @part = null + @loaded = false + @loading = null + @loadingText = null template = $('#template-jam-track-preview') throw "no jam track preview template" if not template.exists() - @root.html($(template.html())) + @root.html(context._.template(template.html(), @options, {variable:'data'})) @playButton = @root.find('.play-button') @stopButton = @root.find('.stop-button') @instrumentIcon = @root.find('.instrument-icon') @instrumentName = @root.find('.instrument-name') @part = @root.find('.part') + @loading = @root.find('.loading') + @loadingText = @root.find('.loading-text') @playButton.on('click', @play) @stopButton.on('click', @stop) @@ -54,57 +59,115 @@ context.JK.JamTrackPreview = class JamTrackPreview if @jamTrackTrack.track_type == 'Track' part = "#{@jamTrackTrack.part}" if @jamTrackTrack.part? && @jamTrackTrack.part != instrumentDescription - + @part.text("(#{part})") if part != '' else - if @options.master_shows_duration - duration = 'entire song' - if @jamTrack.duration - duration = "0:00 - #{context.JK.prettyPrintSeconds(@jamTrack.duration)}" - part = duration - else - part = @jamTrack.name + ' by ' + @jamTrack.original_artist + if @options.master_adds_line_break + part = '"' + @jamTrack.name + '"' + ' by ' + @jamTrack.original_artist + + @part.html("#{part}") if part != '' + @part.addClass('adds-line-break') + else + + if @options.master_shows_duration + duration = 'entire song' + if @jamTrack.duration + duration = "#{context.JK.prettyPrintSeconds(@jamTrack.duration)}" + part = duration + else + part = @jamTrack.name + ' by ' + @jamTrack.original_artist + + @part.text("(#{part})") if part != '' + - @part.text("(#{part})") if part != '' if @jamTrackTrack.preview_mp3_url? urls = [@jamTrackTrack.preview_mp3_url] if @jamTrackTrack.preview_ogg_url? urls.push(@jamTrackTrack.preview_ogg_url) + @urls = urls @no_audio = false - @sound = new Howl({ - src: urls, - autoplay: false, - loop: false, - volume: 1.0, - onend: @onHowlerEnd}) else @no_audio = true if @no_audio @playButton.addClass('disabled') @stopButton.addClass('disabled') + else + if @options.preload_master && @jamTrackTrack.track_type == 'Master' + @sound = new Howl({ + src: @urls, + autoplay: false, + loop: false, + volume: 1.0, + preload: true, + onload: @onHowlerLoad + onend: @onHowlerEnd}) + + onDestroyed: () => + @sound.unload() + + removeNowPlaying: () => + context.JK.JamTrackPreview.nowPlaying.splice(this) + if context.JK.JamTrackPreview.nowPlaying.length > 0 + @logger.warn("multiple jamtrack previews playing") + onHowlerEnd: () => @logger.debug("on end $(this)", $(this)) @stopButton.addClass('hidden') @playButton.removeClass('hidden') + @removeNowPlaying() + + onHowlerLoad: () => + @loaded = true + @loading.fadeOut(); + @loadingText.fadeOut(); #addClass('hidden') play: (e) => if e? e.stopPropagation() + $(this).triggerHandler(@EVENTS.PREVIEW_PLAYED) + if @no_audio context.JK.prodBubble(@playButton, 'There is no preview available for this track.', {}, {duration:2000}) else - logger.debug("play issued for jam track preview") + unless @sound? + @root.on('remove', @onDestroyed); + + @sound = new Howl({ + src: @urls, + autoplay: false, + loop: false, + volume: 1.0, + preload: true, + onload: @onHowlerLoad + onend: @onHowlerEnd}) + + unless @loaded + @loading.removeClass('hidden') + @loadingText.removeClass('hidden') + + + @logger.debug("play issued for jam track preview") @sound.play() + for playingSound in context.JK.JamTrackPreview.nowPlaying + playingSound.issueStop() + context.JK.JamTrackPreview.nowPlaying = [] + context.JK.JamTrackPreview.nowPlaying.push(this) @playButton.addClass('hidden') @stopButton.removeClass('hidden') return false + issueStop: () => + @logger.debug("pause issued for jam track preview") + @sound.pause() # stop does not actually stop in windows client + @stopButton.addClass('hidden') + @playButton.removeClass('hidden') + stop: (e) => if e? e.stopPropagation() @@ -112,14 +175,14 @@ context.JK.JamTrackPreview = class JamTrackPreview if @no_audio context.JK.helpBubble(@playButton, 'There is no preview available for this track.', {}, {duration:2000}) else - logger.debug("stop issued for jam track preview") - @sound.stop() - @stopButton.addClass('hidden') - @playButton.removeClass('hidden') + @issueStop() + @removeNowPlaying() + return false +context.JK.JamTrackPreview.nowPlaying = [] diff --git a/web/app/assets/javascripts/jam_track_screen.js.coffee b/web/app/assets/javascripts/jam_track_screen.js.coffee index d2f1f21a0..ac25cc4c8 100644 --- a/web/app/assets/javascripts/jam_track_screen.js.coffee +++ b/web/app/assets/javascripts/jam_track_screen.js.coffee @@ -6,7 +6,8 @@ context.JK.JamTrackScreen=class JamTrackScreen LIMIT = 10 instrument_logo_map = context.JK.getInstrumentIconMap24() - constructor: (@app) -> + constructor: (@app) -> + @EVENTS = context.JK.EVENTS @logger = context.JK.logger @screen = null @content = null @@ -20,14 +21,38 @@ context.JK.JamTrackScreen=class JamTrackScreen @currentPage = 0 @next = null @currentQuery = this.defaultQuery() - @expanded = false - + @expanded = null + @shownHelperBubbles = false + beforeShow:(data) => this.setFilterFromURL() - this.refresh() - + + if context.JK.currentUserId? + @app.user().done((user) => + @user = user + this.refresh() + ).fail((arg) => + @logger.error("app.user.done failed: " + JSON.stringify(arg)) + + @logger.debug(arg.statusCode); + + throw 'fail should not occur if user is available' + ) + else + this.refresh() + unless @shownHelperBubbles + @shownHelperBubbles = true + @startHelperBubbles() + afterShow:(data) => - + context.JK.Tracking.jamtrackBrowseTrack(@app) + + beforeHide: () => + this.clearCtaHelpTimeout() + this.clearBandFilterHelpTimeout() + this.clearMasterHelpTimeout() + this.clearResults(); + events:() => @genre.on 'change', this.search @artist.on 'change', this.search @@ -40,17 +65,123 @@ context.JK.JamTrackScreen=class JamTrackScreen @noMoreJamtracks.hide() @next = null + startHelperBubbles: () => + @showBandFilterHelpTimeout = setTimeout(@showBandFilterHelp, 3500) + + showBandFilterHelp: () => + context.JK.HelpBubbleHelper.jamtrackBrowseBand(@artist.closest('.easydropdown-wrapper'), $('body')) + + @showMasterHelpDueTime = new Date().getTime() + 11000 # 6000 ms for band tooltip to display, and 5 seconds of quiet time + @scroller.on('scroll', @masterHelpScrollWatch) + @scroller.on('scroll', @clearBubbles) + @showMasterHelpTimeout = setTimeout(@showMasterHelp, @masterHelpDueTime()) + + clearBubbles: () => + if @helpBubble? + @helpBubble.btOff() + @helpBubble = null + + # computes when we should show the master help bubble + masterHelpDueTime: () => + dueTime = @showMasterHelpDueTime - new Date().getTime() + if dueTime <= 0 + dueTime = 2000 + dueTime + + + # computes when we should show the master help bubble + ctaHelpDueTime: () => + dueTime = @showCtaHelpDueTime - new Date().getTime() + if dueTime <= 0 + dueTime = 2000 + dueTime + + # if the user scrolls, reset the master help due time + masterHelpScrollWatch: () => + @clearMasterHelpTimeout() + @showMasterHelpTimeout = setTimeout(@showMasterHelp, @masterHelpDueTime() + 2000) + + # if the user scrolls, reset the master help due time + ctaHelpScrollWatch: () => + @clearCtaHelpTimeout() + @showCtaHelpTimeout = setTimeout(@showCtaHelp, @ctaHelpDueTime() + 2000) + + + showCtaHelp: () => + @scroller.off('scroll', @ctaHelpScrollWatch) + @clearCtaHelpTimeout() + + cutoff = @scroller.offset().top; + + @screen.find('.jamtrack-actions').each((i, element) => + $element = $(element) + + if ($element.offset().top >= cutoff) + @helpBubble = context.JK.HelpBubbleHelper.jamtrackBrowseCta($element, $('body')) + return false + else + return true + ) + + showMasterHelp: () => + @scroller.off('scroll', @masterHelpScrollWatch) + @clearMasterHelpTimeout() + + # don't show the help if the user has already clicked a preview + unless @userPreviewed + cutoff = @scroller.offset().top; + + @screen.find('.jamtrack-preview[data-track-type="Master"]').each((i, element) => + $element = $(element) + + if ($element.offset().top >= cutoff) + @helpBubble = context.JK.HelpBubbleHelper.jamtrackBrowseMasterMix($element.find('.play-button'), $('body')) + return false + else + return true + ) + + @showCtaHelpDueTime = new Date().getTime() + 11000 + @scroller.on('scroll', @ctaHelpScrollWatch) + @showCtaHelpTimeout = setTimeout(@showCtaHelp, @ctaHelpDueTime()) # 6000 ms for bubble show time, and 5000ms for delay + + previewPlayed: () => + @userPreviewed = true + + clearCtaHelpTimeout:() => + if @showCtaHelpTimeout? + clearTimeout(@showCtaHelpTimeout) + @showCtaHelpTimeout = null + + clearBandFilterHelpTimeout: () => + if @showBandFilterHelpTimeout? + clearTimeout(@showBandFilterHelpTimeout) + @showBandFilterHelpTimeout = null + + clearMasterHelpTimeout: () => + if @showMasterHelpTimeout? + clearTimeout(@showMasterHelpTimeout) + @showMasterHelpTimeout = null + setFilterFromURL:() => # Grab parms from URL for artist, instrument, and availability parms=this.getParams() if(parms.artist?) @artist.val(parms.artist) + else + @artist.val('') if(parms.instrument?) @instrument.val(parms.instrument) + else + @instrument.val('') if(parms.availability?) @availability.val(parms.availability) - window.history.replaceState({}, "", "/client#/jamtrack") + else + @availability.val('') + + if window.history.replaceState #ie9 proofing + window.history.replaceState({}, "", "/client#/jamtrackBrowse") getParams:() => params = {} @@ -61,19 +192,33 @@ context.JK.JamTrackScreen=class JamTrackScreen for v in raw_vars [key, val] = v.split("=") params[key] = decodeURIComponent(val) - ms params + setFilterState: (state) => + if state + @genre.easyDropDown('enable').removeAttr('disabled') + @artist.easyDropDown('enable').removeAttr('disabled') + @instrument.easyDropDown('enable').removeAttr('disabled') + @availability.easyDropDown('enable').removeAttr('disabled') + else + @genre.easyDropDown('disable').attr('disabled', 'disabled') + @artist.easyDropDown('disable').attr('disabled', 'disabled') + @instrument.easyDropDown('disable').attr('disabled', 'disabled') + @availability.easyDropDown('disable').attr('disabled', 'disabled') + refresh:() => + this.clearResults() @currentQuery = this.buildQuery() that = this - rest.getJamtracks(@currentQuery).done((response) -> - that.clearResults() + this.setFilterState(false) + rest.getJamTracks(@currentQuery).done((response) => that.handleJamtrackResponse(response) - ).fail (jqXHR) -> + ).fail( (jqXHR) => that.clearResults() that.noMoreJamtracks.show() - that.app.notifyServerError jqXHR, 'Jamtrack Unavailable' + that.app.notifyServerError jqXHR, 'Jamtrack Unavailable' + ).always () => + that.setFilterState(true) search:() => this.refresh() @@ -118,7 +263,7 @@ context.JK.JamTrackScreen=class JamTrackScreen # if we less results than asked for, end searching @scroller.infinitescroll 'pause' if @currentPage == 0 and response.jamtracks.length == 0 - @content.append '
There\'s no jamtracks.
' + @content.append 'Loading ...') img: '/assets/shared/spinner.gif' - path: (page) -> - '/api/jamtracks?' + $.param(this.buildQuery()) + path: (page) => + '/api/jamtracks?' + $.param(that.buildQuery()) - }, (json, opts) -> + }, (json, opts) => this.handleJamtrackResponse(json) @scroller.infinitescroll 'resume' @@ -155,42 +301,137 @@ context.JK.JamTrackScreen=class JamTrackScreen addToCartJamtrack:(e) => e.preventDefault() - params = id: $(e.target).attr('data-jamtrack-id') - rest.addJamtrackToShoppingCart(params).done((response) -> - context.location = '/client#/shoppingCart' + $target = $(e.target) + params = id: $target.attr('data-jamtrack-id') + isFree = $(e.target).is('.is_free') + + rest.addJamtrackToShoppingCart(params).done((response) => + if(isFree) + if context.JK.currentUserId? + alert("TODO") + context.JK.currentUserFreeJamTrack = true # make sure the user sees no more free notices + context.location = '/client#/redeemComplete' + else + # now make a rest call to buy it + context.location = '/client#/redeemSignup' + + else + context.location = '/client#/shoppingCart' + ).fail @app.ajaxError licenseUSWhy:(e) => e.preventDefault() - @app.layout.showDialog 'jamtrack-availability-dialog' + @app.layout.showDialog 'jamtrack-availability-dialog' + + handleExpanded:(trackElement) => + jamTrack = trackElement.data('jamTrack') + expanded = trackElement.data('expanded') + expand = !expanded + trackElement.data('expanded', expand) + + detailArrow = trackElement.find('.jamtrack-detail-btn') + + if expand + trackElement.find('.extra').removeClass('hidden') + detailArrow.html('hide tracks ') + for track in jamTrack.tracks + trackElement.find("[jamtrack-track-id='#{track.id}']").removeClass('hidden') + else + trackElement.find('.extra').addClass('hidden') + detailArrow.html('show all tracks ') + count = 0 + for track in jamTrack.tracks + if count < 6 + trackElement.find("[jamtrack-track-id='#{track.id}']").removeClass('hidden') + else + trackElement.find("[jamtrack-track-id='#{track.id}']").addClass('hidden') + count++ + + + registerEvents:(parent) => + #@screen.find('.jamtrack-detail-btn').on 'click', this.showJamtrackDescription + parent.find('.play-button').on 'click', this.playJamtrack + parent.find('.jamtrack-add-cart').on 'click', this.addToCartJamtrack + parent.find('.license-us-why').on 'click', this.licenseUSWhy + parent.find('.jamtrack-detail-btn').on 'click', this.toggleExpanded + # @screen.find('.jamtrack-preview').each (index, element) => + # new JK.JamTrackPreview(data.app, $element, jamTrack, track, {master_shows_duration: true}) + + rerenderJamtracks:() => + if @currentData? + @clearResults() + @renderJamtracks(@currentData) + false + + computeWeight: (jam_track_track, instrument) => + weight = switch + when jam_track_track.track_type == 'Master' then 0 + when jam_track_track.instrument?.id == instrument then 1 + jam_track_track.position + else 10000 + jam_track_track.position - registerEvents:() => - @screen.find('.jamtrack-detail-btn').on 'click', this.showJamtrackDescription - @screen.find('.play-button').on 'click', this.playJamtrack - @screen.find('.jamtrack-add-cart').on 'click', this.addToCartJamtrack - @screen.find('.license-us-why').on 'click', this.licenseUSWhy - @screen.find('.jamtrack-detail-btn').on 'click', this.toggleExpanded - renderJamtracks:(data) => + @currentData = data that = this + for jamtrack in data.jamtracks + jamtrackExpanded = this.expanded==jamtrack.id + trackRow = _.clone(jamtrack) + trackRow.track_cnt = jamtrack.tracks.length + trackRow.tracks = [] + + # if an instrument is selected by the user, then re-order any jam tracks with a matching instrument to the top + instrument = @instrument.val() + if instrument? + jamtrack.tracks.sort((a, b) => + aWeight = @computeWeight(a, instrument) + bWeight = @computeWeight(b, instrument) + return aWeight - bWeight + ) + for track in jamtrack.tracks - continue if track.track_type=='Master' - 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 + ')' + trackRow.tracks.push(track) + if track.track_type=='Master' + track.instrument_desc = "Master" + else + inst = '../assets/content/icon_instrument_default24.png' + if track.instrument? + if track.instrument.id in instrument_logo_map + inst = instrument_logo_map[track.instrument.id].asset + track.instrument_desc = track.instrument.description + track.instrument_url = inst + + if track.part != '' + track.instrument_desc += ' (' + track.part + ')' + + free_state = if context.JK.currentUserFreeJamTrack then 'free' else 'non-free' + + is_free = free_state == 'free' options = - jamtrack: jamtrack - expanded: that.expanded - + jamtrack: trackRow + expanded: false + free_state: free_state, + is_free: is_free @jamtrackItem = $(context._.template($('#template-jamtrack').html(), options, variable: 'data')) - that.renderJamtrack(@jamtrackItem) - this.registerEvents() + that.renderJamtrack(@jamtrackItem, jamtrack) + that.registerEvents(@jamtrackItem) + + + renderJamtrack:(jamtrackElement, jamTrack) => + jamtrackElement.data('jamTrack', jamTrack) + jamtrackElement.data('expanded', true) + + @content.append jamtrackElement + + #if this.expanded==jamTrack.id + for track in jamTrack.tracks + trackRow = jamtrackElement.find("[jamtrack-track-id='#{track.id}']") + previewElement = trackRow.find(".jamtrack-preview") + preview = new JK.JamTrackPreview(@app, previewElement, jamTrack, track, {master_shows_duration: true, color:'gray'}) + $(preview).on(@EVENTS.PREVIEW_PLAYED, @previewPlayed) + + this.handleExpanded(jamtrackElement, false) showJamtrackDescription:(e) => e.preventDefault() @@ -200,18 +441,19 @@ context.JK.JamTrackScreen=class JamTrackScreen else @description.hide() - toggleExpanded:() => - this.expanded = !this.expanded - this.refresh() - - renderJamtrack:(jamtrack) => - @content.append jamtrack + toggleExpanded:(e) => + e.preventDefault() + jamtrackRecord = $(e.target).parents('.jamtrack-record') + jamTrackId = jamtrackRecord.attr("jamtrack-id") + this.handleExpanded(jamtrackRecord) + initialize:() => screenBindings = 'beforeShow': this.beforeShow 'afterShow': this.afterShow - @app.bindScreen 'jamtrack', screenBindings + 'beforeHide' : this.beforeHide + @app.bindScreen 'jamtrackBrowse', screenBindings @screen = $('#jamtrack-find-form') @scroller = @screen.find('.content-body-scroller') @content = @screen.find('.jamtrack-content') @@ -235,8 +477,8 @@ context.JK.JamTrackScreen=class JamTrackScreen throw new Error('@artist must be specified') if @instrument.length == 0 throw new Error('@instrument must be specified') - if @availability.length == 0 - throw new Error('@availability must be specified') + #if @availability.length == 0 + # throw new Error('@availability must be specified') this.events() diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js index 2bc31b957..d1b41d451 100644 --- a/web/app/assets/javascripts/jamkazam.js +++ b/web/app/assets/javascripts/jamkazam.js @@ -344,6 +344,18 @@ return userData; } + this.refreshUser = function() { + userDeferred = rest.getUserDetail(); + if (userDeferred) { + userDeferred.done(this.updateUserCache) + } + else { + userDeferred = new $.Deferred(); + userDeferred.reject('not_logged_in'); + } + return userDeferred; + } + this.activeElementEvent = function(evtName, data) { return this.layout.activeElementEvent(evtName, data); } diff --git a/web/app/assets/javascripts/jamtrack.js.coffee b/web/app/assets/javascripts/jamtrack.js.coffee deleted file mode 100644 index 95a1c89f0..000000000 --- a/web/app/assets/javascripts/jamtrack.js.coffee +++ /dev/null @@ -1,243 +0,0 @@ -$ = jQuery -context = window -context.JK ||= {} - -context.JK.JamTrackScreen=class JamTrackScreen - LIMIT = 10 - instrument_logo_map = context.JK.getInstrumentIconMap24() - - constructor: (@app) -> - @logger = context.JK.logger - @screen = null - @content = null - @scroller = null - @genre = null - @artist = null - @instrument = null - @availability = null - @nextPager = null - @noMoreJamtracks = null - @currentPage = 0 - @next = null - @currentQuery = this.defaultQuery() - @expanded = false - - beforeShow:(data) => - this.setFilterFromURL() - this.refresh() - - afterShow:(data) => - - events:() => - @genre.on 'change', this.search - @artist.on 'change', this.search - @instrument.on 'change', this.search - @availability.on 'change', this.search - - clearResults:() => - #$logger.debug("CLEARING CONTENT") - @currentPage = 0 - @content.empty() - @noMoreJamtracks.hide() - @next = null - - setFilterFromURL:() => - # Grab parms from URL for artist, instrument, and availability - parms=this.getParams() - this.logger.debug("parms", parms) - if(parms.artist?) - @artist.val(parms.artist) - if(parms.instrument?) - @instrument.val(parms.instrument) - if(parms.availability?) - @availability.val(parms.availability) - window.history.replaceState({}, "", "/client#/jamtrack") - - getParams:() => - params = {} - q = window.location.href.split("?")[1] - if q? - q = q.split('#')[0] - raw_vars = q.split("&") - for v in raw_vars - [key, val] = v.split("=") - params[key] = decodeURIComponent(val) - params - - refresh:() => - @currentQuery = this.buildQuery() - that = this - rest.getJamtracks(@currentQuery).done((response) -> - that.clearResults() - that.handleJamtrackResponse(response) - ).fail (jqXHR) -> - that.clearResults() - that.noMoreJamtracks.show() - that.app.notifyServerError jqXHR, 'Jamtrack Unavailable' - - search:() => - this.refresh() - false - - defaultQuery:() => - query = - per_page: LIMIT - page: @currentPage+1 - if @next - query.since = @next - query - - buildQuery:() => - @currentQuery = this.defaultQuery() - # genre filter - # var genres = @screen.find('#jamtrack_genre').val() - # if (genres !== undefined) { - # @currentQuery.genre = genres - # } - # instrument filter - - instrument = @instrument.val() - if instrument? - @currentQuery.instrument = instrument - - # artist filter - art = @artist.val() - if art? - @currentQuery.artist = art - - # availability filter - availability = @availability.val() - if availability? - @currentQuery.availability = availability - @currentQuery - - handleJamtrackResponse:(response) => - #logger.debug("Handling response", JSON.stringify(response)) - @next = response.next - this.renderJamtracks(response) - if response.next == null - # if we less results than asked for, end searching - @scroller.infinitescroll 'pause' - if @currentPage == 0 and 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++ - this.buildQuery() - this.registerInfiniteScroll() - - - 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: (page) -> - '/api/jamtracks?' + $.param(this.buildQuery()) - - }, (json, opts) -> - this.handleJamtrackResponse(json) - @scroller.infinitescroll 'resume' - - playJamtrack:(e) => - e.preventDefault() - - addToCartJamtrack:(e) => - e.preventDefault() - params = id: $(e.target).attr('data-jamtrack-id') - rest.addJamtrackToShoppingCart(params).done((response) -> - context.location = '/client#/shoppingCart' - ).fail @app.ajaxError - - licenseUSWhy:(e) => - e.preventDefault() - @app.layout.showDialog 'jamtrack-availability-dialog' - - registerEvents:() => - @screen.find('.jamtrack-detail-btn').on 'click', this.showJamtrackDescription - @screen.find('.play-button').on 'click', this.playJamtrack - @screen.find('.jamtrack-add-cart').on 'click', this.addToCartJamtrack - @screen.find('.license-us-why').on 'click', this.licenseUSWhy - @screen.find('.jamtrack-detail-btn').on 'click', this.toggleExpanded - - renderJamtracks:(data) => - that = this - for jamtrack in data.jamtracks - for track in jamtrack.tracks - continue if track.track_type=='Master' - 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 + ')' - - options = - jamtrack: jamtrack - expanded: that.expanded - - @jamtrackItem = $(context._.template($('#template-jamtrack').html(), options, variable: 'data')) - that.renderJamtrack(@jamtrackItem) - this.registerEvents() - - showJamtrackDescription:(e) => - e.preventDefault() - @description = $(e.target).parent('.detail-arrow').next() - if @description.css('display') == 'none' - @description.show() - else - @description.hide() - - toggleExpanded:() => - this.expanded = !this.expanded - this.refresh() - - renderJamtrack:(jamtrack) => - @content.append jamtrack - - initialize:() => - screenBindings = - 'beforeShow': this.beforeShow - 'afterShow': this.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') - @artist = @screen.find('#jamtrack_artist') - @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 new Error('@screen must be specified') - if @scroller.length == 0 - throw new Error('@scroller must be specified') - if @content.length == 0 - throw new Error('@content must be specified') - if @noMoreJamtracks.length == 0 - throw new Error('@noMoreJamtracks must be specified') - #if(@genre.length == 0) throw new Error("@genre must be specified") - - if @artist.length == 0 - throw new Error('@artist must be specified') - if @instrument.length == 0 - throw new Error('@instrument must be specified') - if @availability.length == 0 - throw new Error('@availability must be specified') - this.events() - - diff --git a/web/app/assets/javascripts/jamtrack_landing.js.coffee b/web/app/assets/javascripts/jamtrack_landing.js.coffee index 760744e9f..5198a1f64 100644 --- a/web/app/assets/javascripts/jamtrack_landing.js.coffee +++ b/web/app/assets/javascripts/jamtrack_landing.js.coffee @@ -8,46 +8,72 @@ context.JK.JamTrackLanding = class JamTrackLanding @client = context.jamClient @logger = context.JK.logger @screen = null + @noFreeJamTrack = null + @freeJamTrack = null + @bandList = null + @noBandsFound = null initialize:() => screenBindings = 'beforeShow': @beforeShow 'afterShow': @afterShow @app.bindScreen('jamtrackLanding', screenBindings) - @screen = $('#jamtrackLanding') - + @screen = $('#jamtrackLanding') + @noFreeJamTrack = @screen.find('.no-free-jamtrack') + @freeJamTrack = @screen.find('.free-jamtrack') + @bandList = @screen.find('#band_list') + @noBandsFound = @screen.find('#no_bands_found') + beforeShow:() => + + @noFreeJamTrack.addClass('hidden') + @freeJamTrack.addClass('hidden') + + afterShow:() => + + if context.JK.currentUserId + @app.user().done(@onUser) + else + @onUser({free_jamtrack: gon.global.one_free_jamtrack_per_user}) + + onUser:(user) => + if user.free_jamtrack + @freeJamTrack.removeClass('hidden') + else + @noFreeJamTrack.removeClass('hidden') + # Get artist names and build links - @rest.getJamtracks({group_artist: true}) - .done(this.buildArtistLinks) - .fail(this.handleFailure) + @rest.getJamTrackArtists({group_artist: true, per_page:100}) + .done(this.buildArtistLinks) + .fail(this.handleFailure) # Bind links to action that will open the jam_tracks list view filtered to given artist_name: # artist_name this.bindArtistLinks() - afterShow:() => buildArtistLinks:(response) => - # Get artist names and build links - jamtracks = response.jamtracks + # Get artist names and build links + @logger.debug("buildArtest links response", response) + + artists = response.artists $("#band_list>li:not('#no_bands_found')").remove() - if jamtracks.length==0 - $("#no_bands_found").removeClass("hidden") + if artists.length==0 + @noBandsFound.removeClass("hidden") else - $("#no_bands_found").addClass("hidden") + @noBandsFound.addClass("hidden") # client#/jamtrack - for jamtrack in jamtracks - artistLink = "#{jamtrack.original_artist}" - $("#band_list").append("
  • #{artistLink}
  • ") + for artist in artists + artistLink = "#{artist.original_artist} (#{artist.song_count})" + @bandList.append("
  • #{artistLink}
  • ") # We don't want to do a full page load if this is clicked on here: bindArtistLinks:() => - band_list=$("ul#band_list") that=this - band_list.on "click", "a.artist-link", (event)-> - context.location="client#/jamtrack" - window.history.replaceState({}, "", this.href) + @bandList.on "click", "a.artist-link", (event)-> + context.location="client#/jamtrackBrowse" + if window.history.replaceState # ie9 proofing + window.history.replaceState({}, "", this.href) event.preventDefault() handleFailure:(error) => diff --git a/web/app/assets/javascripts/jquery.listenRecording.js b/web/app/assets/javascripts/jquery.listenRecording.js index 84a7628f7..1303f50f7 100644 --- a/web/app/assets/javascripts/jquery.listenRecording.js +++ b/web/app/assets/javascripts/jquery.listenRecording.js @@ -78,6 +78,10 @@ audioDomElement.play(); isPlaying = true; rest.addPlayablePlay(recordingId, 'JamRuby::Recording', claimedRecordingId, context.JK.currentUserId); + rest.getRecording({id: recordingId}) + .done(function(recording) { + context.JK.GA.trackRecordingPlay(recording); + }) }) } diff --git a/web/app/assets/javascripts/jquery.listenbroadcast.js b/web/app/assets/javascripts/jquery.listenbroadcast.js index f6964cd60..89c5184dd 100644 --- a/web/app/assets/javascripts/jquery.listenbroadcast.js +++ b/web/app/assets/javascripts/jquery.listenbroadcast.js @@ -112,7 +112,8 @@ waitForBufferingTimeout = setTimeout(noBuffer, WAIT_FOR_BUFFER_TIMEOUT); logger.debug("setting buffering timeout"); rest.addPlayablePlay(musicSessionId, 'JamRuby::MusicSession', null, context.JK.currentUserId); - + context.JK.GA.trackJamTrackPlaySession(musicSessionId, false) + if(needsCanPlayGuard()) { $audio.bind('canplay', function() { audioDomElement.play(); diff --git a/web/app/assets/javascripts/landing/landing.js b/web/app/assets/javascripts/landing/landing.js index 0afe196f0..de23401db 100644 --- a/web/app/assets/javascripts/landing/landing.js +++ b/web/app/assets/javascripts/landing/landing.js @@ -1,3 +1,4 @@ +//= require bugsnag //= require bind-polyfill //= require jquery //= require jquery.monkeypatch @@ -32,8 +33,8 @@ //= require jamkazam //= require utils //= require ui_helper -//= require ga //= require jam_rest +//= require ga //= require web/signup_helper //= require web/signin_helper //= require web/signin diff --git a/web/app/assets/javascripts/landing/signup.js b/web/app/assets/javascripts/landing/signup.js index a91329222..0eec67cf4 100644 --- a/web/app/assets/javascripts/landing/signup.js +++ b/web/app/assets/javascripts/landing/signup.js @@ -156,6 +156,9 @@ submit_data.city = $('#jam_ruby_user_city').val() submit_data.birth_date = gather_birth_date() submit_data.instruments = gather_instruments() + if($.QueryString['affiliate_partner_id']) { + submit_data.affiliate_partner_id = $.QueryString['affiliate_partner_id']; + } //submit_data.photo_url = $('#jam_ruby_user_instruments').val() diff --git a/web/app/assets/javascripts/paginator.js b/web/app/assets/javascripts/paginator.js index 963aee1e3..a25be9696 100644 --- a/web/app/assets/javascripts/paginator.js +++ b/web/app/assets/javascripts/paginator.js @@ -66,7 +66,7 @@ function registerEvents(paginator) { $('a.page-less', paginator).click(function(e) { - var currentPage = parseInt($(this).attr('data-current-page')); + var currentPage = parseInt(paginator.attr('data-current-page')); if (currentPage > 0) { var targetPage = currentPage - 1; attemptToMoveToTargetPage(targetPage); @@ -78,7 +78,8 @@ }); $('a.page-more', paginator).click(function(e) { - var currentPage = parseInt($(this).attr('data-current-page')); + + var currentPage = parseInt(paginator.attr('data-current-page')); if (currentPage < pages - 1) { var targetPage = currentPage + 1; attemptToMoveToTargetPage(targetPage); diff --git a/web/app/assets/javascripts/playbackControls.js b/web/app/assets/javascripts/playbackControls.js index d56a1e214..84ec5aed6 100644 --- a/web/app/assets/javascripts/playbackControls.js +++ b/web/app/assets/javascripts/playbackControls.js @@ -29,11 +29,13 @@ var $playButton = $('.play-button img.playbutton', $parentElement); var $pauseButton = $('.play-button img.pausebutton', $parentElement); + var $stopButton = $('.stop-button img.stopbutton', $parentElement); var $currentTime = $('.recording-current', $parentElement); var $duration = $('.duration-time', $parentElement); var $sliderBar = $('.recording-playback', $parentElement); var $slider = $('.recording-slider', $parentElement); var $playmodeButton = $('.playback-mode-buttons.icheckbuttons input', $parentElement); + var $jamTrackGetReady = $('.jam-track-get-ready', $parentElement); var $self = $(this); @@ -41,6 +43,7 @@ var playbackDurationMs = 0; var playbackPositionMs = 0; var durationChanged = false; + var seenActivity = false; var endReached = false; var dragging = false; @@ -50,18 +53,38 @@ var playbackMode = PlaybackMode.EveryWhere; var monitorPlaybackTimeout = null; var playbackMonitorMode = PLAYBACK_MONITOR_MODE.MEDIA_FILE; + var monitoring = false; + + function init() { + updateSliderPosition(0); + updateDurationTime(0); + updateCurrentTime(0); + seenActivity = false; + } function startPlay() { + seenActivity = false; updateIsPlaying(true); if(endReached) { update(0, playbackDurationMs, playbackPlaying); } $self.triggerHandler('play', {playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode}); + + + if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) { + var sessionModel = context.JK.CurrentSessionModel || null; + context.JK.GA.trackJamTrackPlaySession(sessionModel.id(), true) + } } function stopPlay(endReached) { updateIsPlaying(false); - $self.triggerHandler('pause', {playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode, endReached : endReached}); + $self.triggerHandler('stop', {playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode, endReached : endReached}); + } + + function pausePlay(endReached) { + updateIsPlaying(false); + $self.triggerHandler('pause', {playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode, endReached : endReached}); } function updateOffsetBasedOnPosition(offsetLeft) { @@ -78,10 +101,10 @@ function startDrag(e, ui) { dragging = true; playingWhenDragStart = playbackPlaying; - draggingUpdateTimer = setInterval(function() { canUpdateBackend = true; }, 333); // only call backend up to 3 times a second while dragging - if(playingWhenDragStart) { - stopPlay(); - } + //draggingUpdateTimer = setInterval(function() { canUpdateBackend = true; }, 333); // only call backend up to 3 times a second while dragging + //if(playingWhenDragStart) { + //stopPlay(); + //} } function stopDrag(e, ui) { @@ -91,11 +114,12 @@ canUpdateBackend = true; updateOffsetBasedOnPosition(ui.position.left); + updateSliderPosition(playbackPositionMs); - if(playingWhenDragStart) { - playingWhenDragStart = false; - startPlay(); - } + //if(playingWhenDragStart) { + // playingWhenDragStart = false; + // startPlay(); + //} } function onDrag(e, ui) { @@ -120,6 +144,17 @@ // return false; //} + pausePlay(); + return false; + }); + + $stopButton.on('click', function(e) { + var sessionModel = context.JK.CurrentSessionModel || null; + //if(sessionModel && sessionModel.areControlsLockedForJamTrackRecording() && $parentElement.closest('.session-track').data('track_data').type == 'jam_track') { + // context.JK.prodBubble($pauseButton, 'jamtrack-controls-disabled', {}, {positions:['top'], offsetParent: $pauseButton}) + // return false; + //} + stopPlay(); return false; }); @@ -158,7 +193,9 @@ setPlaybackMode(playmode); }); - function styleControls( ) { + function styleControls() { + $jamTrackGetReady.attr('data-mode', playbackMonitorMode); + $parentElement.removeClass('mediafile-mode jamtrack-mode metronome-mode'); if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.MEDIA_FILE) { $parentElement.addClass('mediafile-mode'); @@ -175,12 +212,14 @@ } } function monitorRecordingPlayback() { + if(!monitoring) { + return; + } if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) { var positionMs = context.jamClient.SessionCurrrentJamTrackPlayPosMs(); var duration = context.jamClient.SessionGetJamTracksPlayDurationMs(); var durationMs = duration.media_len; - var start = duration.start; // needed to understand start offset, and prevent slider from moving in tapins - //console.log("JamTrack start: " + start) + var start = duration.start; // needed to understand start offset, and prevent slider from moving in tapins } else { var positionMs = context.jamClient.SessionCurrrentPlayPosMs(); @@ -194,6 +233,11 @@ positionMs = 0; } + if(positionMs > 0) { + seenActivity = true; + } + + if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.METRONOME) { updateIsPlaying(isPlaying); } @@ -201,6 +245,17 @@ update(positionMs, durationMs, isPlaying); } + if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) { + + if(playbackPlaying) { + $jamTrackGetReady.attr('data-current-time', positionMs) + } + else { + // this is so the jamtrack 'Get Ready!' stays hidden when it's not playing + $jamTrackGetReady.attr('data-current-time', -1) + } + + } monitorPlaybackTimeout = setTimeout(monitorRecordingPlayback, 500); } @@ -212,9 +267,9 @@ } // at the end of the play, the duration sets to 0, as does currentTime. but isPlaying does not reset to - logger.debug("currentTimeMs, durationTimeMs", currentTimeMs, durationTimeMs); - if(currentTimeMs == 0 && durationTimeMs == 0) { - if(isPlaying) { + //logger.debug("currentTimeMs, durationTimeMs, mode", currentTimeMs, durationTimeMs, playbackMonitorMode); + if(currentTimeMs == 0 && seenActivity) { + if(playbackPlaying) { isPlaying = false; durationTimeMs = playbackDurationMs; currentTimeMs = playbackDurationMs; @@ -223,6 +278,8 @@ logger.debug("end reached"); } else { + // make sure slide shows '0' + updateCurrentTime(currentTimeMs); return; } } @@ -278,7 +335,8 @@ $pauseButton.hide(); } - playbackPlaying = isPlaying; + logger.debug("updating is playing: " + isPlaying) + playbackPlaying = isPlaying; } } @@ -304,6 +362,9 @@ } function startMonitor(_playbackMonitorMode) { + monitoring = true; + // resets everything to zero + init(); if(_playbackMonitorMode === undefined || _playbackMonitorMode === null) { playbackMonitorMode = PLAYBACK_MONITOR_MODE.MEDIA_FILE; @@ -319,6 +380,7 @@ } function stopMonitor() { + monitoring = false; logger.debug("playbackControl.stopMonitor") if(monitorPlaybackTimeout!= null) { clearTimeout(monitorPlaybackTimeout); @@ -326,11 +388,28 @@ } } + function onPlayStartEvent() { + updateIsPlaying(true); + playbackPlaying = true; + seenActivity = false; + } + + function onPlayStopEvent() { + updateIsPlaying(false); + playbackPlaying = false; + } + + function onPlayPauseEvent() { + playbackPlaying = false; + } this.update = update; this.setPlaybackMode = setPlaybackMode; this.startMonitor = startMonitor; this.stopMonitor = stopMonitor; + this.onPlayStopEvent = onPlayStopEvent; + this.onPlayStartEvent = onPlayStartEvent; + this.onPlayPauseEvent = onPlayPauseEvent; return this; } diff --git a/web/app/assets/javascripts/react-components.js b/web/app/assets/javascripts/react-components.js new file mode 100644 index 000000000..e534d5c8c --- /dev/null +++ b/web/app/assets/javascripts/react-components.js @@ -0,0 +1,9 @@ +//= require_self +//= require react_ujs + +React = require('react'); + +// note that this is a global assignment, it will be discussed further below +DemoComponent = require('./components/DemoComponent'); + +// //= require_tree ./react-components diff --git a/web/app/assets/javascripts/react-components/.gitkeep b/web/app/assets/javascripts/react-components/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/web/app/assets/javascripts/react-components/DemoComponent.jsx b/web/app/assets/javascripts/react-components/DemoComponent.jsx new file mode 100644 index 000000000..59d296fbe --- /dev/null +++ b/web/app/assets/javascripts/react-components/DemoComponent.jsx @@ -0,0 +1,10 @@ +var React = require('react'); + +var DemoComponent = React.createClass({displayName: 'Demo Component', + render: function() { + return
    Demo Component
    ; + } +}); + +// each file will export exactly one component +module.exports = DemoComponent; \ No newline at end of file diff --git a/web/app/assets/javascripts/recordingModel.js b/web/app/assets/javascripts/recordingModel.js index a0e275f48..4f0b84e00 100644 --- a/web/app/assets/javascripts/recordingModel.js +++ b/web/app/assets/javascripts/recordingModel.js @@ -56,6 +56,7 @@ } currentRecording = null; currentRecordingId = null; + stoppingRecording = false; } @@ -81,6 +82,7 @@ $self.triggerHandler('startingRecording', {}); currentlyRecording = true; + stoppingRecording = false; currentRecording = rest.startRecording({"music_session_id": sessionModel.id()}) .done(function(recording) { @@ -103,6 +105,13 @@ /** Nulls can be passed for all 3 currently; that's a user request. */ function stopRecording(recordingId, reason, detail) { + if(stoppingRecording) { + logger.debug("ignoring stopRecording because we are already stopping"); + return; + } + stoppingRecording = true; + + waitingOnServerStop = waitingOnClientStop = true; waitingOnStopTimer = setTimeout(timeoutTransitionToStop, 5000); @@ -115,7 +124,14 @@ var groupedTracks = groupTracksToClient(recording); + //if(sessionModel.jamTracks() && isRecording()) { + // logger.debug("preemptive stop media") + //context.jamClient.SessionStopPlay(); + //} + + logger.debug("stopping recording") jamClient.StopRecording(recording.id, groupedTracks); + rest.stopRecording( { "id": recording.id } ) .done(function() { waitingOnServerStop = false; @@ -149,6 +165,7 @@ // Only tell the user that we've stopped once both server and client agree we've stopped function attemptTransitionToStop(recordingId, errorReason, errorDetail) { + if(!waitingOnClientStop && !waitingOnServerStop) { transitionToStopped(); $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: errorReason, detail: errorDetail}); diff --git a/web/app/assets/javascripts/redeem_complete.js b/web/app/assets/javascripts/redeem_complete.js new file mode 100644 index 000000000..6d5ec9a3d --- /dev/null +++ b/web/app/assets/javascripts/redeem_complete.js @@ -0,0 +1,263 @@ +(function (context, $) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.RedeemCompleteScreen = function (app) { + + var EVENTS = context.JK.EVENTS; + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var jamTrackUtils = context.JK.JamTrackUtils; + var checkoutUtils = context.JK.CheckoutUtilsInstance; + + var $screen = null; + var $navigation = null; + var $templatePurchasedJamTrack = null; + var $thanksPanel = null; + var $jamTrackInBrowser = null; + var $jamTrackInClient = null; + var $purchasedJamTrack = null; + var $purchasedJamTrackHeader = null; + var $purchasedJamTracks = null; + var userDetail = null; + var step = null; + var downloadJamTracks = []; + var purchasedJamTracks = null; + var purchasedJamTrackIterator = 0; + var $backBtn = null; + var $downloadApplicationLink = null; + var $noPurchasesPrompt = null; + var shoppingCartItem = null; + + + function beforeShow() { + + } + + function afterShow(data) { + $noPurchasesPrompt.addClass('hidden') + $purchasedJamTracks.empty() + $thanksPanel.addClass("hidden") + $purchasedJamTrackHeader.attr('status', 'in-progress') + $jamTrackInBrowser.addClass('hidden') + $jamTrackInClient.addClass('hidden') + + + // if there is no current user, but it apperas we have a login cookie, just refresh + if(!context.JK.currentUserId && $.cookie('remember_token')) { + window.location.reload(); + } + else { + redeemJamTrack() + } + + //prepThanks() + } + + function handleShoppingCartResponse(carts) { + + if(!checkoutUtils.hasOneFreeItemInShoppingCart(carts)) { + // the user has multiple items in their shopping cart. They shouldn't be here. + logger.error("invalid access of redeemComplete page") + window.location = '/client#/jamtrackBrowse' + } + else { + // ok, we have one, free item. save it for + shoppingCartItem = carts[0]; + + rest.placeOrder() + .done(function(purchaseResponse) { + context.JK.currentUserFreeJamTrack = false // make sure the user sees no more free notices without having to do a full page refresh + + checkoutUtils.setLastPurchase(purchaseResponse) + jamTrackUtils.checkShoppingCart() + //app.refreshUser() // this only causes grief in tests for some reason, and with currentUserFreeJamTrack = false above, this is probably now unnecessary + + prepThanks(); + }) + .fail(function() { + + }) + } + } + + function redeemJamTrack() { + rest.getShoppingCarts() + .done(handleShoppingCartResponse) + .fail(app.ajaxError); + + } + + function beforeHide() { + if(downloadJamTracks) { + context._.each(downloadJamTracks, function(downloadJamTrack) { + downloadJamTrack.destroy(); + downloadJamTrack.root.remove(); + }) + + downloadJamTracks = []; + } + purchasedJamTracks = null; + purchasedJamTrackIterator = 0; + } + + function prepThanks() { + showThanks(); + } + + function showThanks(purchaseResponse) { + + + var purchaseResponse = checkoutUtils.getLastPurchase(); + + if(!purchaseResponse || purchaseResponse.length == 0) { + // user got to this page with no context + logger.debug("no purchases found; nothing to show") + $noPurchasesPrompt.removeClass('hidden') + } + else { + if(gon.isNativeClient) { + $jamTrackInClient.removeClass('hidden') + } + else { + $jamTrackInBrowser.removeClass('hidden'); + } + $thanksPanel.removeClass('hidden') + handleJamTracksPurchased(purchaseResponse.jam_tracks) + } + } + + function handleJamTracksPurchased(jamTracks) { + // were any JamTracks purchased? + var jamTracksPurchased = jamTracks && jamTracks.length > 0; + if(jamTracksPurchased) { + if(gon.isNativeClient) { + $jamTrackInClient.removeClass('hidden') + context.JK.GA.virtualPageView('/redeemInClient'); + startDownloadJamTracks(jamTracks) + } + else { + $jamTrackInBrowser.removeClass('hidden'); + + app.user().done(function(user) { + // relative to 1 day ago (24 * 60 * 60 * 1000) + if(new Date(user.created_at).getTime() < new Date().getTime() - 86400000) { + logger.debug("existing user recorded") + context.JK.GA.virtualPageView('/redeemInBrowserExistingUser'); + } + else { + logger.debug("new user recorded") + context.JK.GA.virtualPageView('/redeemInBrowserNewUser'); + } + }) + + + + app.user().done(function(user) { + if(!user.first_downloaded_client_at) { + $downloadApplicationLink.removeClass('hidden') + } + }) + } + } + } + + function startDownloadJamTracks(jamTracks) { + // there can be multiple purchased JamTracks, so we cycle through them + + purchasedJamTracks = jamTracks; + + // populate list of jamtracks purchased, that we will iterate through graphically + context._.each(jamTracks, function(jamTrack) { + var downloadJamTrack = new context.JK.DownloadJamTrack(app, jamTrack, 'small'); + var $purchasedJamTrack = $(context._.template( + $templatePurchasedJamTrack.html(), + jamTrack, + {variable: 'data'} + )); + + $purchasedJamTracks.append($purchasedJamTrack) + + // show it on the page + $purchasedJamTrack.append(downloadJamTrack.root) + + downloadJamTracks.push(downloadJamTrack) + }) + + iteratePurchasedJamTracks(); + } + + function iteratePurchasedJamTracks() { + if(purchasedJamTrackIterator < purchasedJamTracks.length ) { + var downloadJamTrack = downloadJamTracks[purchasedJamTrackIterator++]; + + // make sure the 'purchasing JamTrack' section can be seen + $purchasedJamTrack.removeClass('hidden'); + + // the widget indicates when it gets to any transition; we can hide it once it reaches completion + $(downloadJamTrack).on(EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, function(e, data) { + + if(data.state == downloadJamTrack.states.synchronized) { + logger.debug("jamtrack " + downloadJamTrack.jamTrack.name + " synchronized;") + //downloadJamTrack.root.remove(); + downloadJamTrack.destroy(); + + // go to the next JamTrack + iteratePurchasedJamTracks() + } + }) + + logger.debug("jamtrack " + downloadJamTrack.jamTrack.name + " downloader initializing") + + // kick off the download JamTrack process + downloadJamTrack.init() + + // XXX style-test code + // downloadJamTrack.transitionError("package-error", "The server failed to create your package.") + + } + else { + logger.debug("done iterating over purchased JamTracks") + $purchasedJamTrackHeader.attr('status', 'done') + } + } + + function events() { + $backBtn.on('click', function(e) { + e.preventDefault(); + + context.location = '/client#/jamtrackBrowse' + }) + } + + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow, + 'beforeHide': beforeHide + }; + app.bindScreen('redeemComplete', screenBindings); + + $screen = $("#redeemCompleteScreen"); + $templatePurchasedJamTrack = $('#template-purchased-jam-track'); + $thanksPanel = $screen.find(".thanks-panel"); + $jamTrackInBrowser = $screen.find(".jam-tracks-in-browser"); + $jamTrackInClient = $screen.find(".jam-tracks-in-client"); + $purchasedJamTrack = $thanksPanel.find(".thanks-detail.purchased-jam-track"); + $purchasedJamTrackHeader = $purchasedJamTrack.find(".purchased-jam-track-header"); + $purchasedJamTracks = $purchasedJamTrack.find(".purchased-list") + $backBtn = $screen.find('.back'); + $downloadApplicationLink = $screen.find('.download-jamkazam-wrapper'); + $noPurchasesPrompt = $screen.find('.no-purchases-prompt') + + if ($screen.length == 0) throw "$screen must be specified"; + + events(); + } + + this.initialize = initialize; + + return this; + } +}) +(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/redeem_signup.js b/web/app/assets/javascripts/redeem_signup.js new file mode 100644 index 000000000..e0785321f --- /dev/null +++ b/web/app/assets/javascripts/redeem_signup.js @@ -0,0 +1,245 @@ +(function(context,$) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.RedeemSignUpScreen = function(app) { + + var logger = context.JK.logger; + var rest = context.JK.Rest(); + + var $screen = null; + var $signupForm = null; + var $self = $(this); + var $email = null; + var $password = null; + var $firstName = null; + var $lastName = null; + + var $signupBtn = null; + var $inputElements = null; + var $contentHolder = null; + var $btnNext = null; + var $btnFacebook = null; + var $termsOfServiceL = null; + var $termsOfServiceR = null; + var shoppingCartItem = null; + var $jamtrackName = null; + var $signinLink = null; + + function beforeShow(data) { + renderLoggedInState(); + } + + function afterShow(data) { + + } + + + function renderLoggedInState(){ + + if(isLoggedIn()) { + $contentHolder.removeClass('not-signed-in').addClass('signed-in') + } + else { + context.JK.Tracking.redeemSignupTrack(app) + $jamtrackName.text('') + $contentHolder.addClass('hidden') + $contentHolder.removeClass('signed-in').addClass('not-signed-in') + // now check that the user has one, and only one, free jamtrack in their shopping cart. + rest.getShoppingCarts() + .done(handleShoppingCartResponse) + .fail(app.ajaxError); + } + } + + function isLoggedIn() { + return !!context.JK.currentUserId; + } + + function events() { + $btnFacebook.on('click', onClickSignupFacebook) + $signupForm.on('submit', signup) + $signupBtn.on('click', signup) + $signinLink.on('click', onSignin); + } + + function handleShoppingCartResponse(carts) { + + if(carts.length == 0) { + // nothing is in the user's shopping cart. They shouldn't be here. + logger.error("invalid access of redeemJamTrack page") + window.location = '/client#/jamtrackBrowse' + } + else if(carts.length > 1) { + // the user has multiple items in their shopping cart. They shouldn't be here. + logger.error("invalid access of redeemJamTrack page") + window.location = '/client#/jamtrackBrowse' + } + else { + var item = carts[0]; + + if(item.product_info.free) { + // ok, we have one, free item. save it for + shoppingCartItem = item; + $jamtrackName.text('"' + shoppingCartItem.product_info.name + '"') + $contentHolder.removeClass('hidden') + } + else { + // the user has a non-free, single item in their basket. They shouldn't be here. + logger.error("invalid access of redeemJamTrack page") + window.location = '/client#/jamtrackBrowse' + } + } + + var $latestCartHtml = ""; + + var any_in_us = false + context._.each(carts, function(cart) { + if(cart.product_info.sales_region == 'United States') { + any_in_us = true + } + }) + } + function onClickSignupFacebook(e) { + // tos must already be clicked + + $btnFacebook.addClass('disabled') + + var $field = $termsOfServiceL.closest('.field') + $field.find('.error-text').remove() + + logger.debug("field, ", $field, $termsOfServiceL) + if($termsOfServiceL.is(":checked")) { + + rest.createSignupHint({redirect_location: '/client#/redeemComplete'}) + .done(function() { + // send the user on to facebook signin + window.location = $btnFacebook.attr('href'); + }) + .fail(function() { + app.notify({text:"Facebook Signup is not working properly"}); + }) + .always(function() { + $btnFacebook.removeClass('disabled') + }) + } + else { + $field.addClass("error").addClass("transparent"); + $field.append("
    • must be accepted
    "); + } + return false; + } + + function onSuccessfulSignin() { + // the user has signed in; + + // move all shopping carts from the anonymous user to the signed in user + /*rest.portOverCarts() + .done(function() { + logger.debug("ported over carts") + window.location = '/client#/redeemComplete' + }) + .fail(function() { + window.location.reload(); + }) + +*/ + window.location = '/client#/redeemComplete' + } + + function onSignin() { + app.layout.showDialog('signin-dialog', {redirect_to:onSuccessfulSignin}); + return false; + } + + function signup() { + if($signupBtn.is('.disabled')) { + return false; + } + + // clear out previous errors + $signupForm.find('.field.error').removeClass('error') + $signupForm.find('ul.error-text').remove() + + var email = $email.val(); + var password = $password.val(); + var first_name = $firstName.val(); + var last_name = $lastName.val(); + var terms = $termsOfServiceR.is(':checked') + + $signupBtn.text('TRYING...').addClass('disabled') + + + rest.signup({email: email, password: password, first_name: first_name, last_name: last_name, terms:terms}) + .done(function(response) { + window.location = '/client#/redeemComplete' + window.location.reload() + }) + .fail(function(jqXHR) { + if(jqXHR.status == 422) { + var response = JSON.parse(jqXHR.responseText) + if(response.errors) { + var $errors = context.JK.format_errors('first_name', response); + if ($errors) $firstName.closest('.field').addClass('error').append($errors); + + $errors = context.JK.format_errors('last_name', response); + if ($errors) $lastName.closest('.field').addClass('error').append($errors); + + $errors = context.JK.format_errors('password', response); + if ($errors) $password.closest('.field').addClass('error').append($errors); + + var $errors = context.JK.format_errors('email', response); + if ($errors) $email.closest('.field').addClass('error').append($errors); + + var $errors = context.JK.format_errors('terms_of_service', response); + if ($errors) $termsOfServiceR.closest('.field').addClass('error').append($errors); + } + else { + app.notify({title: 'Unknown Signup Error', text: jqXHR.responseText}) + } + } + else { + app.notifyServerError(jqXHR, "Unable to Sign Up") + } + }) + .always(function() { + $signupBtn.text('SIGNUP').removeClass('disabled') + }) + + + return false; + } + + + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow + }; + app.bindScreen('redeemSignup', screenBindings); + + $screen = $("#redeemSignupScreen"); + $signupForm = $screen.find(".signup-form"); + $signupBtn = $signupForm.find('.signup-submit'); + $email = $signupForm.find('input[name="email"]'); + $password = $signupForm.find('input[name="password"]'); + $firstName = $signupForm.find('input[name="first_name"]'); + $lastName = $signupForm.find('input[name="last_name"]'); + $inputElements = $signupForm.find('.input-elements'); + $contentHolder = $screen.find('.content-holder'); + $btnFacebook = $screen.find('.signup-facebook') + $termsOfServiceL = $screen.find('.left-side .terms_of_service input[type="checkbox"]') + $termsOfServiceR = $screen.find('.right-side .terms_of_service input[type="checkbox"]') + $jamtrackName = $screen.find('.jamtrack-name'); + $signinLink = $screen.find('.signin') + + if($screen.length == 0) throw "$screen must be specified"; + + events(); + } + + this.initialize = initialize; + + return this; + } +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/scheduled_session.js.erb b/web/app/assets/javascripts/scheduled_session.js.erb index cf75275e1..11891e938 100644 --- a/web/app/assets/javascripts/scheduled_session.js.erb +++ b/web/app/assets/javascripts/scheduled_session.js.erb @@ -5,7 +5,7 @@ context.JK = context.JK || {}; context.JK.CreateScheduledSession = function(app) { - var gearUtils = context.JK.GearUtils; + var gearUtils = context.JK.GearUtilsInstance; var sessionUtils = context.JK.SessionUtils; var logger = context.JK.logger; var rest = JK.Rest(); @@ -597,7 +597,9 @@ if(willOptionStartSession()) { - gearUtils.guardAgainstInvalidConfiguration(app) + var shouldVerifyNetwork = createSessionSettings.musician_access.value != 'only-rsvp'; + + gearUtils.guardAgainstInvalidConfiguration(app, shouldVerifyNetwork) .fail(function() { $btn.removeClass('disabled') app.notify( @@ -908,11 +910,31 @@ createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_QUICK_START %>'; } + function optionRequiresMultiplayerProfile() { + return createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_START_SCHEDULED%>' || + createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_IMMEDIATE %>' || + createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_RSVP %>' || + createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_SCHEDULE_FUTURE %>'; + } + function next(event) { if(willOptionStartSession()) { if(!context.JK.guardAgainstBrowser(app)) { return false; } + + if(createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_QUICK_START %>') { + // short-cut added for private sessions; just get it going + beforeMoveStep1(); // this will populate the createSessionSettings structure + startSessionClicked(); // and this will create the session + return false; + } + } + + if(optionRequiresMultiplayerProfile()) { + // if(context.JK.guardAgainstSinglePlayerProfile(app).canPlay == false) { + // return false; + //} } var valid = beforeMoveStep(); @@ -971,7 +993,11 @@ } function afterShow() { + context.JK.HelpBubbleHelper.jamtrackGuidePrivate($screen.find('li[create-type="quick-start"] label'), $screen.find('.content-body-scroller')) + } + function beforeHide() { + context.JK.HelpBubbleHelper.clearJamTrackGuide(); } function toggleDate(dontRebuildDropdowns) { @@ -1054,7 +1080,9 @@ context.JK.GenreSelectorHelper.render('#create-session-genre'); - //inviteMusiciansUtil.loadFriends(); + if(context.JK.currentUserId) { + inviteMusiciansUtil.loadFriends(); + } context.JK.dropdown($screen.find('#session-musician-access')); context.JK.dropdown($screen.find('#session-fans-access')); @@ -1278,7 +1306,7 @@ instrumentSelector = instrumentSelectorInstance; instrumentRSVP = instrumentRSVPSelectorInstance; - var screenBindings = {'beforeShow': beforeShow, 'afterShow': afterShow}; + var screenBindings = {'beforeShow': beforeShow, 'afterShow': afterShow, 'beforeHide' : beforeHide}; app.bindScreen('createSession', screenBindings); $wizardSteps = $screen.find('.create-session-wizard'); diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index 2b6ea1e80..a00991840 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -13,7 +13,8 @@ var modUtils = context.JK.ModUtils; var logger = context.JK.logger; var self = this; - + var webcamViewer = new context.JK.WebcamViewer() + var defaultParticipant = { tracks: [{ instrument_id: "unknown" @@ -93,6 +94,7 @@ var claimedRecording = null; var backing_track_path = null; var jamTrack = null; + var musicianAccessOnJoin; // was this a private or public session when the user tried to joined? var metronomeMixer = null; var playbackControls = null; @@ -107,6 +109,8 @@ var $screen = null; var $mixModeDropdown = null; var $templateMixerModeChange = null; + + var $myTracksNoTracks = null; var $otherAudioContainer = null; var $myTracksContainer = null; var $liveTracksContainer = null; @@ -120,6 +124,9 @@ var $liveTracks = null; var $audioTracks = null; var $fluidTracks = null; + var $voiceChat = null; + var $openFtue = null; + var $tracksHolder = null; var mediaTrackGroups = [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup]; var muteBothMasterAndPersonalGroups = [ChannelGroupIds.AudioInputMusicGroup, ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup]; @@ -128,16 +135,19 @@ var RENDER_SESSION_DELAY = 750; // When I need to render a session, I have to wait a bit for the mixers to be there. function beforeShow(data) { - sessionId = data.id; - if(!sessionId) { - window.location = '/client#/home'; - } - promptLeave = true; - $myTracksContainer.empty(); - displayDoneRecording(); // assumption is that you can't join a recording session, so this should be safe + sessionId = data.id; + if(!sessionId) { + window.location = '/client#/home'; + } + promptLeave = true; + $myTracksContainer.empty(); + displayDoneRecording(); // assumption is that you can't join a recording session, so this should be safe - var shareDialog = new JK.ShareDialog(context.JK.app, sessionId, "session"); - shareDialog.initialize(context.JK.FacebookHelperInstance); + var shareDialog = new JK.ShareDialog(context.JK.app, sessionId, "session"); + shareDialog.initialize(context.JK.FacebookHelperInstance); + if(gon.global.video_available && gon.global.video_available!="none") { + webcamViewer.beforeShow() + } } function beforeDisconnect() { @@ -163,10 +173,13 @@ } } checkForCurrentUser(); + + context.JK.HelpBubbleHelper.jamtrackGuideSession($screen.find('li.open-a-jamtrack'), $screen) } function afterShow(data) { + $fluidTracks.addClass('showing'); $openBackingTrack.removeClass('disabled'); if(!context.JK.JamServer.connected) { @@ -196,53 +209,64 @@ // body-scoped drag handlers can go active screenActive = true; - gearUtils.guardAgainstInvalidConfiguration(app) + rest.getSessionHistory(data.id) + .done(function(musicSession) { + + musicianAccessOnJoin = musicSession.musician_access;; + + var shouldVerifyNetwork = musicSession.musician_access; + gearUtils.guardAgainstInvalidConfiguration(app, shouldVerifyNetwork) + .fail(function() { + promptLeave = false; + window.location = '/client#/home' + }) + .done(function(){ + var result = sessionUtils.SessionPageEnter(); + + gearUtils.guardAgainstActiveProfileMissing(app, result) + .fail(function(data) { + promptLeave = false; + if(data && data.reason == 'handled') { + if(data.nav == 'BACK') { + window.history.go(-1); + } + else { + window.location = data.nav; + } + } + else { + window.location = '/client#/home'; + } + }) + .done(function(){ + + sessionModel.waitForSessionPageEnterDone() + .done(function(userTracks) { + + context.JK.CurrentSessionModel.setUserTracks(userTracks); + + initializeSession(); + }) + .fail(function(data) { + if(data == "timeout") { + context.JK.alertSupportedNeeded('The audio system has not reported your configured tracks in a timely fashion.') + } + else if(data == 'session_over') { + // do nothing; session ended before we got the user track info. just bail + } + else { + context.JK.alertSupportedNeeded('Unable to determine configured tracks due to reason: ' + data) + } + promptLeave = false; + window.location = '/client#/home' + }); + }) + }) + }) .fail(function() { - promptLeave = false; - window.location = '/client#/home' + }) - .done(function(){ - var result = sessionUtils.SessionPageEnter(); - gearUtils.guardAgainstActiveProfileMissing(app, result) - .fail(function(data) { - promptLeave = false; - if(data && data.reason == 'handled') { - if(data.nav == 'BACK') { - window.history.go(-1); - } - else { - window.location = data.nav; - } - } - else { - window.location = '/client#/home'; - } - }) - .done(function(){ - - sessionModel.waitForSessionPageEnterDone() - .done(function(userTracks) { - - context.JK.CurrentSessionModel.setUserTracks(userTracks); - - initializeSession(); - }) - .fail(function(data) { - if(data == "timeout") { - context.JK.alertSupportedNeeded('The audio system has not reported your configured tracks in a timely fashion.') - } - else if(data == 'session_over') { - // do nothing; session ended before we got the user track info. just bail - } - else { - contetx.JK.alertSupportedNeeded('Unable to determine configured tracks due to reason: ' + data) - } - promptLeave = false; - window.location = '/client#/home' - }); - }) - }) } function notifyWithUserInfo(title , text, clientId) { @@ -265,188 +289,229 @@ function afterCurrentUserLoaded() { - var sessionModel = context.JK.CurrentSessionModel; - $(sessionModel.recordingModel) + // now check if the user can play in a session with others + var deferred = new $.Deferred(); + if(musicianAccessOnJoin) { + deferred = context.JK.guardAgainstSinglePlayerProfile(app, function () { + promptLeave = false; + }); + } + else { + deferred.resolve(); + } + deferred.fail(function(result) { + if(!result.controlled_location) { + window.location="/client#/home" + } + }) + .done(function() { + logger.debug("user has passed all session guards") + promptLeave = true; + var sessionModel = context.JK.CurrentSessionModel; + + $(sessionModel.recordingModel) .on('startingRecording', function(e, data) { - displayStartingRecording(); - lockControlsforJamTrackRecording(); + displayStartingRecording(); + lockControlsforJamTrackRecording(); }) .on('startedRecording', function(e, data) { - if(data.reason) { - var reason = data.reason; - var detail = data.detail; - - var title = "Could Not Start Recording"; - - if(data.reason == 'client-no-response') { - notifyWithUserInfo(title, 'did not respond to the start signal.', detail); - } - else if(data.reason == 'empty-recording-id') { - app.notifyAlert(title, "No recording ID specified."); - } - else if(data.reason == 'missing-client') { - notifyWithUserInfo(title, 'could not be signalled to start recording.', detail); - } - else if(data.reason == 'already-recording') { - app.notifyAlert(title, 'Already recording. If this appears incorrect, try restarting JamKazam.'); - } - else if(data.reason == 'recording-engine-unspecified') { - notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail); - } - else if(data.reason == 'recording-engine-create-directory') { - notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail); - } - else if(data.reason == 'recording-engine-create-file') { - notifyWithUserInfo(title, 'had a problem creating a recording file.', detail); - } - else if(data.reason == 'recording-engine-sample-rate') { - notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail); - } - else if(data.reason == 'rest') { - var jqXHR = detail[0]; - app.notifyServerError(jqXHR); - } - else { - notifyWithUserInfo(title, 'Error Reason: ' + reason); - } - displayDoneRecording(); - } - else - { - displayStartedRecording(); - displayWhoCreated(data.clientId); - lockControlsforJamTrackRecording(); - } - }) - .on('stoppingRecording', function(e, data) { - displayStoppingRecording(data); - unlockControlsforJamTrackRecording(); - }) - .on('stoppedRecording', function(e, data) { - - unlockControlsforJamTrackRecording(); - if(sessionModel.selfOpenedJamTracks()) { - - var timeline = context.jamClient.GetJamTrackTimeline(); - - rest.addRecordingTimeline(data.recordingId, timeline) - .fail(function(){ - app.notify( - { title: "Unable to Add JamTrack Volume Data", - text: "The volume of the JamTrack will not be correct in the recorded mix." }, - null, - true); - }) - } - - if(data.reason) { - logger.warn("Recording Discarded: ", data); - var reason = data.reason; - var detail = data.detail; - - var title = "Recording Discarded"; - - if(data.reason == 'client-no-response') { - notifyWithUserInfo(title, 'did not respond to the stop signal.', detail); - } - else if(data.reason == 'missing-client') { - notifyWithUserInfo(title, 'could not be signalled to stop recording.', detail); - } - else if(data.reason == 'empty-recording-id') { - app.notifyAlert(title, "No recording ID specified."); - } - else if(data.reason == 'wrong-recording-id') { - app.notifyAlert(title, "Wrong recording ID specified."); - } - else if(data.reason == 'not-recording') { - app.notifyAlert(title, "Not currently recording."); - } - else if(data.reason == 'already-stopping') { - app.notifyAlert(title, "Already stopping the current recording."); - } - else if(data.reason == 'start-before-stop') { - notifyWithUserInfo(title, 'asked that we start a new recording; cancelling the current one.', detail); - } - else { - app.notifyAlert(title, "Error reason: " + reason); - } - - displayDoneRecording(); - } - else { - displayDoneRecording(); - promptUserToSave(data.recordingId); - } - - }) - .on('abortedRecording', function(e, data) { + if(data.reason) { var reason = data.reason; var detail = data.detail; - var title = "Recording Cancelled"; + var title = "Could Not Start Recording"; if(data.reason == 'client-no-response') { - notifyWithUserInfo(title, 'did not respond to the start signal.', detail); + notifyWithUserInfo(title, 'did not respond to the start signal.', detail); + } + else if(data.reason == 'empty-recording-id') { + app.notifyAlert(title, "No recording ID specified."); } else if(data.reason == 'missing-client') { - notifyWithUserInfo(title, 'could not be signalled to start recording.', detail); + notifyWithUserInfo(title, 'could not be signalled to start recording.', detail); } - else if(data.reason == 'populate-recording-info') { - notifyWithUserInfo(title, 'could not synchronize with the server.', detail); + else if(data.reason == 'already-recording') { + app.notifyAlert(title, 'Already recording. If this appears incorrect, try restarting JamKazam.'); } else if(data.reason == 'recording-engine-unspecified') { - notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail); + notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail); } else if(data.reason == 'recording-engine-create-directory') { - notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail); + notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail); } else if(data.reason == 'recording-engine-create-file') { - notifyWithUserInfo(title, 'had a problem creating a recording file.', detail); + notifyWithUserInfo(title, 'had a problem creating a recording file.', detail); } else if(data.reason == 'recording-engine-sample-rate') { - notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail); + notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail); + } + else if(data.reason == 'rest') { + var jqXHR = detail[0]; + app.notifyServerError(jqXHR); } else { - app.notifyAlert(title, "Error reason: " + reason); + notifyWithUserInfo(title, 'Error Reason: ' + reason); + } + displayDoneRecording(); + } + else + { + displayStartedRecording(); + displayWhoCreated(data.clientId); + lockControlsforJamTrackRecording(); + } + }) + .on('stoppingRecording', function(e, data) { + displayStoppingRecording(data); + unlockControlsforJamTrackRecording(); + }) + .on('stoppedRecording', function(e, data) { + + unlockControlsforJamTrackRecording(); + if(sessionModel.selfOpenedJamTracks()) { + + var timeline = context.jamClient.GetJamTrackTimeline(); + + rest.addRecordingTimeline(data.recordingId, timeline) + .fail(function(){ + app.notify( + { title: "Unable to Add JamTrack Volume Data", + text: "The volume of the JamTrack will not be correct in the recorded mix." }, + null, + true); + }) + } + + if(data.reason) { + logger.warn("Recording Discarded: ", data); + var reason = data.reason; + var detail = data.detail; + + var title = "Recording Discarded"; + + if(data.reason == 'client-no-response') { + notifyWithUserInfo(title, 'did not respond to the stop signal.', detail); + } + else if(data.reason == 'missing-client') { + notifyWithUserInfo(title, 'could not be signalled to stop recording.', detail); + } + else if(data.reason == 'empty-recording-id') { + app.notifyAlert(title, "No recording ID specified."); + } + else if(data.reason == 'wrong-recording-id') { + app.notifyAlert(title, "Wrong recording ID specified."); + } + else if(data.reason == 'not-recording') { + app.notifyAlert(title, "Not currently recording."); + } + else if(data.reason == 'already-stopping') { + app.notifyAlert(title, "Already stopping the current recording."); + } + else if(data.reason == 'start-before-stop') { + notifyWithUserInfo(title, 'asked that we start a new recording; cancelling the current one.', detail); + } + else { + app.notifyAlert(title, "Error reason: " + reason); } displayDoneRecording(); + } + else { + displayDoneRecording(); + promptUserToSave(data.recordingId, timeline); + } + + }) + .on('abortedRecording', function(e, data) { + var reason = data.reason; + var detail = data.detail; + + var title = "Recording Cancelled"; + + if(data.reason == 'client-no-response') { + notifyWithUserInfo(title, 'did not respond to the start signal.', detail); + } + else if(data.reason == 'missing-client') { + notifyWithUserInfo(title, 'could not be signalled to start recording.', detail); + } + else if(data.reason == 'populate-recording-info') { + notifyWithUserInfo(title, 'could not synchronize with the server.', detail); + } + else if(data.reason == 'recording-engine-unspecified') { + notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail); + } + else if(data.reason == 'recording-engine-create-directory') { + notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail); + } + else if(data.reason == 'recording-engine-create-file') { + notifyWithUserInfo(title, 'had a problem creating a recording file.', detail); + } + else if(data.reason == 'recording-engine-sample-rate') { + notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail); + } + else { + app.notifyAlert(title, "Error reason: " + reason); + } + + displayDoneRecording(); }) - sessionModel.subscribe('sessionScreen', sessionChanged); - sessionModel.joinSession(sessionId) + sessionModel.subscribe('sessionScreen', sessionChanged); + + sessionModel.joinSession(sessionId) .fail(function(xhr, textStatus, errorMessage) { - if(xhr.status == 404) { - // we tried to join the session, but it's already gone. kick user back to join session screen - promptLeave = false; - context.window.location = "/client#/findSession"; - app.notify( - { title: "Unable to Join Session", - text: "The session you attempted to join is over." - }, - null, - true); + if(xhr.status == 404) { + // we tried to join the session, but it's already gone. kick user back to join session screen + promptLeave = false; + context.window.location = "/client#/findSession"; + app.notify( + { title: "Unable to Join Session", + text: "The session you attempted to join is over." + }, + null, + true); + } + else if(xhr.status == 422) { + var response = JSON.parse(xhr.responseText); + if(response["errors"] && response["errors"]["tracks"] && response["errors"]["tracks"][0] == "Please select at least one track") { + app.notifyAlert("No Inputs Configured", $('You will need to reconfigure your audio device.')); } - else if(xhr.status == 422) { - var response = JSON.parse(xhr.responseText); - if(response["errors"] && response["errors"]["tracks"] && response["errors"]["tracks"][0] == "Please select at least one track") { - app.notifyAlert("No Inputs Configured", $('You will need to reconfigure your audio device.')); - } - else if(response["errors"] && response["errors"]["music_session"] && response["errors"]["music_session"][0] == ["is currently recording"]) { - promptLeave = false; - context.window.location = "/client#/findSession"; - app.notify( { title: "Unable to Join Session", text: "The session is currently recording." }, null, true); - } - else { - app.notifyServerError(xhr, 'Unable to Join Session'); - } + else if(response["errors"] && response["errors"]["music_session"] && response["errors"]["music_session"][0] == ["is currently recording"]) { + promptLeave = false; + context.window.location = "/client#/findSession"; + app.notify( { title: "Unable to Join Session", text: "The session is currently recording." }, null, true); } else { - app.notifyServerError(xhr, 'Unable to Join Session'); + app.notifyServerError(xhr, 'Unable to Join Session'); } - }); + } + else { + app.notifyServerError(xhr, 'Unable to Join Session'); + } + }) + .done(function() { + // check if this is a auto-load jamtrack situation (came from account jamtrack screen) + var jamTrack = sessionUtils.grabAutoOpenJamTrack(); + if(jamTrack) { + // give the session to settle just a little (call a timeout of 1 second) + setTimeout(function() { + // tell the server we are about to open a jamtrack + rest.openJamTrack({id: context.JK.CurrentSessionModel.id(), jam_track_id: jamTrack.id}) + .done(function(response) { + // now actually load the jamtrack + context.JK.CurrentSessionModel.updateSession(response); + loadJamTrack(jamTrack); + }) + .fail(function(jqXHR) { + app.notifyServerError(jqXHR, "Unable to Open JamTrack For Playback"); + }) + }, 1000) + } + }) + }) + } // not leave session but leave screen @@ -464,6 +529,13 @@ function beforeHide(data) { + context.JK.HelpBubbleHelper.clearJamTrackGuide(); + + if(gon.global.video_available && gon.global.video_available!="none") { + webcamViewer.setVideoOff() + } + + $fluidTracks.removeClass('showing'); if(screenActive) { // this path is possible if FTUE is invoked on session page, and they cancel sessionModel.leaveCurrentSession() @@ -495,6 +567,7 @@ var metronomeMasterMixers = getMetronomeMasterMixers(); if (metronomeMixer == null && metronomeMasterMixers.length > 0) { + logger.debug("monitoring metronome") playbackControls.startMonitor(context.JK.PLAYBACK_MONITOR_MODE.METRONOME) } else if (metronomeMixer != null && metronomeMasterMixers.length == 0) { @@ -505,10 +578,14 @@ function checkJamTrackTransition(currentSession) { // handle jam tracks - if (jamTrack == null && (currentSession && currentSession.jam_track != null)) { + + // if we have a recording open, then don't go into JamTrack monitor mode even if a JamTrack is open + if (jamTrack == null && (currentSession && currentSession.jam_track != null && currentSession.claimed_recording == null)) { + logger.debug("monitoring jamtrack") playbackControls.startMonitor(context.JK.PLAYBACK_MONITOR_MODE.JAMTRACK); } - else if (jamTrack && (currentSession == null || currentSession.jam_track == null)) { + else if (jamTrack && (currentSession == null || (currentSession.jam_track == null && currentSession.claimed_recording == null))) { + logger.debug("stop monitoring jamtrack") playbackControls.stopMonitor(); } jamTrack = currentSession == null ? null : currentSession.jam_track; @@ -517,9 +594,11 @@ function checkBackingTrackTransition(currentSession) { // handle backing tracks if (backing_track_path == null && (currentSession && currentSession.backing_track_path != null)) { + logger.debug("monitoring backing track") playbackControls.startMonitor(); } else if (backing_track_path && (currentSession == null || currentSession.backing_track_path == null)) { + logger.debug("stop monitoring backing track") playbackControls.stopMonitor(); } backing_track_path = currentSession == null ? null : currentSession.backing_track_path; @@ -531,9 +610,11 @@ if (claimedRecording == null && (currentSession && currentSession.claimed_recording != null)) { // this is a 'started with a claimed_recording' transition. // we need to start a timer to watch for the state of the play session + logger.debug("monitoring recording") playbackControls.startMonitor(); } else if (claimedRecording && (currentSession == null || currentSession.claimed_recording == null)) { + logger.debug("stop monitoring recording") playbackControls.stopMonitor(); } claimedRecording = currentSession == null ? null : currentSession.claimed_recording; @@ -635,7 +716,6 @@ function renderSession() { $myTracksContainer.empty(); $('.session-track').remove(); // Remove previous tracks - var $voiceChat = $('#voice-chat'); $voiceChat.hide(); _updateMixers(); _renderTracks(); @@ -673,7 +753,7 @@ function _initDialogs() { configureTrackDialog.initialize(); - addNewGearDialog.initialize(); + addNewGearDialog.initialize(); } // Get the latest list of underlying audio mixer channels, and populates: @@ -933,7 +1013,6 @@ if(voiceChatMixers) { var mixer = voiceChatMixers.mixer; - var $voiceChat = $('#voice-chat'); $voiceChat.show(); $voiceChat.attr('mixer-id', mixer.id); var $voiceChatGain = $voiceChat.find('.voicechat-gain'); @@ -1216,9 +1295,10 @@ }); } - function renderJamTracks(jamTrackMixers) { - logger.debug("rendering jam tracks") + function renderJamTracks(jamTrackMixersOrig) { + logger.debug("rendering jam tracks", jamTrackMixersOrig); + var jamTrackMixers = jamTrackMixersOrig.slice(); var jamTracks = [] var jamTrackName = 'JamTrack'; if(sessionModel.isPlayingRecording()) { @@ -1243,13 +1323,15 @@ $('.session-recording-name').text(jamTrackName); var noCorrespondingTracks = false; - $.each(jamTrackMixers, function(index, mixer) { + $.each(jamTracks, function(index, jamTrack) { + var mixer = null; var preMasteredClass = ""; // find the track or tracks that correspond to the mixer var correspondingTracks = [] - $.each(jamTracks, function(i, jamTrack) { - if(mixer.id == jamTrack.id) { + $.each(jamTrackMixersOrig, function(i, matchMixer) { + if(matchMixer.id == jamTrack.id) { correspondingTracks.push(jamTrack); + mixer = matchMixer; } }); @@ -1268,6 +1350,9 @@ return $.inArray(value, correspondingTracks) < 0; }); + // prune found mixers + jamTrackMixers.splice(mixer); + var oneOfTheTracks = correspondingTracks[0]; var instrumentIcon = context.JK.getInstrumentIcon45(oneOfTheTracks.instrument.id); var photoUrl = "/assets/content/icon_recording.png"; @@ -1654,79 +1739,89 @@ var myTrack = app.clientId == participant.client_id; + // special case; if it's me and I have no tracks, show info about this sort of use of the app + if (myTrack && participant.tracks.length == 0) { + $tracksHolder.addClass('no-local-tracks') + $liveTracksContainer.addClass('no-local-tracks') + } + else { + $tracksHolder.removeClass('no-local-tracks') + $liveTracksContainer.removeClass('no-local-tracks') + } + // loop through all tracks for each participant - $.each(participant.tracks, function(index, track) { - var instrumentIcon = context.JK.getInstrumentIcon45(track.instrument_id); - var photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url); + $.each(participant.tracks, function (index, track) { + var instrumentIcon = context.JK.getInstrumentIcon45(track.instrument_id); + var photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url); - // Default trackData to participant + no Mixer state. - var trackData = { - trackId: track.id, - connection_id: track.connection_id, - client_track_id: track.client_track_id, - client_resource_id: track.client_resource_id, - clientId: participant.client_id, - name: name, - instrumentIcon: instrumentIcon, - avatar: photoUrl, - latency: "good", - gainPercent: 0, - muteClass: 'muted', - mixerId: "", - avatarClass: 'avatar-med', - preMasteredClass: "", - myTrack: myTrack - }; + // Default trackData to participant + no Mixer state. + var trackData = { + trackId: track.id, + connection_id: track.connection_id, + client_track_id: track.client_track_id, + client_resource_id: track.client_resource_id, + clientId: participant.client_id, + name: name, + instrumentIcon: instrumentIcon, + avatar: photoUrl, + latency: "good", + gainPercent: 0, + muteClass: 'muted', + mixerId: "", + avatarClass: 'avatar-med', + preMasteredClass: "", + myTrack: myTrack + }; - var mixerData = findMixerForTrack(participant.client_id, track, myTrack) - var mixer = mixerData.mixer; - var vuMixer = mixerData.vuMixer; - var muteMixer = mixerData.muteMixer; - var oppositeMixer = mixerData.oppositeMixer; + var mixerData = findMixerForTrack(participant.client_id, track, myTrack) + var mixer = mixerData.mixer; + var vuMixer = mixerData.vuMixer; + var muteMixer = mixerData.muteMixer; + var oppositeMixer = mixerData.oppositeMixer; - - if (mixer && oppositeMixer) { - myTrack = (mixer.group_id === ChannelGroupIds.AudioInputMusicGroup); - if(!myTrack) { - // it only makes sense to track 'audio established' for tracks that don't belong to you - sessionModel.setAudioEstablished(participant.client_id, true); - } - - var gainPercent = percentFromMixerValue( - mixer.range_low, mixer.range_high, mixer.volume_left); - var muteClass = "enabled"; - if (mixer.mute) { - muteClass = "muted"; - } - - trackData.gainPercent = gainPercent; - trackData.muteClass = muteClass; - trackData.mixerId = mixer.id; - trackData.vuMixerId = vuMixer.id; - trackData.oppositeMixer = oppositeMixer; - trackData.muteMixerId = muteMixer.id; - trackData.noaudio = false; - trackData.group_id = mixer.group_id; - context.jamClient.SessionSetUserName(participant.client_id,name); - - } else { // No mixer to match, yet - lookingForMixers.push({track: track, clientId: participant.client_id}) - trackData.noaudio = true; - if (!(lookingForMixersTimer)) { - logger.debug("waiting for mixer to show up for track: " + track.id) - lookingForMixersTimer = context.setInterval(lookForMixers, 500); - } + if (mixer && oppositeMixer) { + myTrack = (mixer.group_id === ChannelGroupIds.AudioInputMusicGroup); + if (!myTrack) { + // it only makes sense to track 'audio established' for tracks that don't belong to you + sessionModel.setAudioEstablished(participant.client_id, true); } - var allowDelete = myTrack && index > 0; - _addTrack(allowDelete, trackData, mixer, oppositeMixer); - - // Show settings icons only for my tracks - if (myTrack) { - myTracks.push(trackData); + var gainPercent = percentFromMixerValue( + mixer.range_low, mixer.range_high, mixer.volume_left); + var muteClass = "enabled"; + if (mixer.mute) { + muteClass = "muted"; } + + trackData.gainPercent = gainPercent; + trackData.muteClass = muteClass; + trackData.mixerId = mixer.id; + trackData.vuMixerId = vuMixer.id; + trackData.oppositeMixer = oppositeMixer; + trackData.muteMixerId = muteMixer.id; + trackData.noaudio = false; + trackData.group_id = mixer.group_id; + context.jamClient.SessionSetUserName(participant.client_id, name); + + } else { // No mixer to match, yet + lookingForMixers.push({track: track, clientId: participant.client_id}) + trackData.noaudio = true; + if (!(lookingForMixersTimer)) { + logger.debug("waiting for mixer to show up for track: " + track.id) + lookingForMixersTimer = context.setInterval(lookForMixers, 500); + } + } + + var allowDelete = myTrack && index > 0; + _addTrack(allowDelete, trackData, mixer, oppositeMixer); + + // Show settings icons only for my tracks + if (myTrack) { + myTracks.push(trackData); + } }); + }); configureTrackDialog = new context.JK.ConfigureTrackDialog(app, myTracks, sessionId, sessionModel); @@ -1879,14 +1974,14 @@ if (!(mixer.stereo)) { // mono track if (mixerId.substr(-4) === "_vul") { // Do the left - selector = $('#tracks [mixer-id="' + pureMixerId + '_vul"]'); + selector = $tracksHolder.find('[mixer-id="' + pureMixerId + '_vul"]'); context.JK.VuHelpers.updateVU(selector, value); // Do the right - selector = $('#tracks [mixer-id="' + pureMixerId + '_vur"]'); + selector = $tracksHolder.find('[mixer-id="' + pureMixerId + '_vur"]'); context.JK.VuHelpers.updateVU(selector, value); } // otherwise, it's a mono track, _vur event - ignore. } else { // stereo track - selector = $('#tracks [mixer-id="' + mixerId + '"]'); + selector = $tracksHolder.find('[mixer-id="' + mixerId + '"]'); context.JK.VuHelpers.updateVU(selector, value); } } @@ -1960,16 +2055,13 @@ var otherAudioWidthPct = Math.floor(100 * otherAudioWidth/totalWidth); var liveTrackWidthPct = Math.ceil(100 * liveTrackWidth/totalWidth); - logger.debug("resizeFluid: ", minimumLiveTrackWidth, otherAudioWidth, otherAudioWidthPct, liveTrackWidthPct, liveTrackWidthPct) - + //logger.debug("resizeFluid: ", minimumLiveTrackWidth, otherAudioWidth, otherAudioWidthPct, liveTrackWidthPct, liveTrackWidthPct) $audioTracks.css('width', otherAudioWidthPct + '%'); $liveTracks.css('width', liveTrackWidthPct + '%'); } function _addRecordingTrack(trackData, mixer, oppositeMixer) { - otherAudioFilled(); - $('.session-recordings .recording-controls').show(); var parentSelector = '#session-recordedtracks-container'; @@ -2429,6 +2521,18 @@ return false; } + function sessionWebCam(e) { + e.preventDefault(); + if(webcamViewer.isVideoShared()) { + $('#session-webcam').removeClass("selected") + } else { + $('#session-webcam').addClass("selected") + } + + webcamViewer.toggleWebcam() + return false; + } + // http://stackoverflow.com/questions/2604450/how-to-create-a-jquery-clock-timer function updateRecordingTimer() { @@ -2468,11 +2572,12 @@ } function displayStartedRecording() { + // the commented out code reflects dropping the counter as your recording to save space startTimeDate = new Date; - $recordingTimer = $("(0:00)"); - var $recordingStatus = $('').append("Stop Recording").append($recordingTimer); + //$recordingTimer = $("(0:00)"); + var $recordingStatus = $('').append("Stop Recording")//.append($recordingTimer); $('#recording-status').html( $recordingStatus ); - recordingTimerInterval = setInterval(updateRecordingTimer, 1000); + //recordingTimerInterval = setInterval(updateRecordingTimer, 1000); } function displayStoppingRecording(data) { @@ -2499,7 +2604,7 @@ $recordingTimer = null; $('#recording-start-stop').removeClass('currently-recording'); - $('#recording-status').text("Make a Recording"); + $('#recording-status').text("Make Recording"); } function lockControlsforJamTrackRecording() { @@ -2530,9 +2635,12 @@ } } - function promptUserToSave(recordingId) { + function promptUserToSave(recordingId, timeline) { rest.getRecording( {id: recordingId} ) .done(function(recording) { + if(timeline) { + recording.timeline = timeline.global + } recordingFinishedDialog.setRecording(recording); app.layout.showDialog('recordingFinished').one(EVENTS.DIALOG_CLOSED, function(e, data) { if(data.result && data.result.keep){ @@ -2544,7 +2652,14 @@ } function checkPendingMetronome() { - logger.debug("checkPendingMetronome", sessionModel.isMetronomeOpen(), getMetronomeMasterMixers().length) + + if(sessionModel.jamTracks() !== null || sessionModel.recordedJamTracks() !== null) { + // ignore all metronome events when jamtracks are open, because backend opens metronome mixer to play jamtrack tap-ins + logger.debug("ignore checkPendingMetronome because JamTrack is open") + return; + } + + //logger.debug("checkPendingMetronome", sessionModel.isMetronomeOpen(), getMetronomeMasterMixers().length) if(sessionModel.isMetronomeOpen() && getMetronomeMasterMixers().length == 0) { var pendingMetronome = $($templatePendingMetronome.html()) @@ -2604,62 +2719,7 @@ // once the dialog is closed, see if the user has a jamtrack selected if(!data.canceled && data.result.jamTrack) { - - var jamTrack = data.result.jamTrack; - - // hide 'other audio' placeholder - otherAudioFilled(); - - if(downloadJamTrack) { - // if there was one showing before somehow, destroy it. - logger.warn("destroying existing JamTrack") - downloadJamTrack.root.remove(); - downloadJamTrack.destroy(); - downloadJamTrack = null - } - - downloadJamTrack = new context.JK.DownloadJamTrack(app, jamTrack, 'large'); - - // the widget indicates when it gets to any transition; we can hide it once it reaches completion - $(downloadJamTrack).on(EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, function(e, data) { - - if(data.state == downloadJamTrack.states.synchronized) { - logger.debug("jamtrack synchronized; hide widget and show tracks") - downloadJamTrack.root.remove(); - downloadJamTrack.destroy(); - downloadJamTrack = null; - - // XXX: test with this removed; it should be unnecessary - context.jamClient.JamTrackStopPlay(); - - if(jamTrack.jmep) - { - logger.debug("setting jmep data") - context.jamClient.JamTrackLoadJmep(jamTrack.id, jamTrack.jmep) - } - else { - logger.debug("no jmep data for jamtrack") - } - - // JamTrackPlay means 'load' - var result = context.jamClient.JamTrackPlay(jamTrack.id); - - if(!result) { - app.notify( - { title: "JamTrack Can Not Open", - text: "Unable to open your JamTrack. Please contact support@jamkazam.com" - }, null, true); - } else { - rest.playJamTrack(jamTrack.id); - } - } - }) - - // show it on the page - $otherAudioContainer.append(downloadJamTrack.root) - - // kick off the download JamTrack process - downloadJamTrack.init() + loadJamTrack(data.result.jamTrack); } else { logger.debug("OpenJamTrack dialog closed with no selection; ignoring", data) @@ -2669,8 +2729,84 @@ return false; } - function openBackingTrackFile(e) { + function loadJamTrack(jamTrack) { + $('.session-recording-name').text(''); + + // hide 'other audio' placeholder + otherAudioFilled(); + + if(downloadJamTrack) { + // if there was one showing before somehow, destroy it. + logger.warn("destroying existing JamTrack") + downloadJamTrack.root.remove(); + downloadJamTrack.destroy(); + downloadJamTrack = null + } + + downloadJamTrack = new context.JK.DownloadJamTrack(app, jamTrack, 'large'); + + // the widget indicates when it gets to any transition; we can hide it once it reaches completion + $(downloadJamTrack).on(EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, function(e, data) { + + if(data.state == downloadJamTrack.states.synchronized) { + logger.debug("jamtrack synchronized; hide widget and show tracks") + downloadJamTrack.root.remove(); + downloadJamTrack.destroy(); + downloadJamTrack = null; + + // XXX: test with this removed; it should be unnecessary + context.jamClient.JamTrackStopPlay(); + + var sampleRate = context.jamClient.GetSampleRate() + var sampleRateForFilename = sampleRate == 48 ? '48' : '44' + var fqId = jamTrack.id + '-' + sampleRateForFilename + + if(jamTrack.jmep) + { + logger.debug("setting jmep data") + + context.jamClient.JamTrackLoadJmep(fqId, jamTrack.jmep) + } + else { + logger.debug("no jmep data for jamtrack") + } + + // JamTrackPlay means 'load' + var result = context.jamClient.JamTrackPlay(fqId); + + if(!result) { + app.notify( + { title: "JamTrack Can Not Open", + text: "Unable to open your JamTrack. Please contact support@jamkazam.com" + }, null, true); + } else { + playJamTrack(jamTrack.id); + } + } + }) + + // show it on the page + $otherAudioContainer.append(downloadJamTrack.root) + + // kick off the download JamTrack process + downloadJamTrack.init() + } + function playJamTrack(jamTrackId) { + var participantCnt=sessionModel.participants().length + rest.playJamTrack(jamTrackId) + .done(function() { + app.refreshUser(); + }) + context.stats.write('web.jamtrack.open', { + value: 1, + session_size: participantCnt, + user_id: context.JK.currentUserId, + user_name: context.JK.currentUserName + }) + }// function + + function openBackingTrackFile(e) { // just ignore the click if they are currently recording for now if(sessionModel.recordingModel.isRecording()) { app.notify({ @@ -2681,6 +2817,12 @@ return false; } else { context.jamClient.openBackingTrackFile(sessionModel.backing_track) + context.stats.write('web.backingtrack.open', { + value: 1, + session_size: participantCnt, + user_id: context.JK.currentUserId, + user_name: context.JK.currentUserName + }) //context.JK.CurrentSessionModel.refreshCurrentSession(true); } return false; @@ -2807,6 +2949,12 @@ } function closeBackingTrack() { + + if (sessionModel.recordingModel.isRecording()) { + logger.debug("can't close backing track while recording") + return false; + } + rest.closeBackingTrack({id: sessionModel.id()}) .done(function() { //sessionModel.refreshCurrentSession(true); @@ -2827,8 +2975,24 @@ } function closeJamTrack() { + logger.debug("closing jam track"); - logger.debug("closing recording"); + if (sessionModel.recordingModel.isRecording()) { + logger.debug("can't close jamtrack while recording") + app.notify({title: 'Can Not Close JamTrack', text: 'A JamTrack can not be closed while recording.'}) + return false; + } + + if(!sessionModel.selfOpenedJamTracks()) { + logger.debug("can't close jamtrack if not the opener") + app.notify({title: 'Can Not Close JamTrack', text: 'Only the person who opened the JamTrack can close it.'}) + return false; + } + + if(!sessionModel.selfOpenedJamTracks()) { + logger.debug("can't close jamtrack if not the opener") + return false; + } if(downloadJamTrack) { logger.debug("closing DownloadJamTrack widget") @@ -2897,13 +3061,33 @@ } function onPause(e, data) { - logger.debug("calling jamClient.SessionStopPlay. endReached:", data.endReached); + + // if a JamTrack is open, and the user hits 'pause' or 'stop', we need to automatically stop the recording + if(sessionModel.jamTracks() && sessionModel.recordingModel.isRecording()) { + logger.debug("preemptive jamtrack stop") + startStopRecording(); + } if(!data.endReached) { - context.jamClient.SessionStopPlay(); + logger.debug("calling jamClient.SessionPausePlay. endReached:", data.endReached); + context.jamClient.SessionPausePlay(); } } + function onStop(e, data) { + + // if a JamTrack is open, and the user hits 'pause' or 'stop', we need to automatically stop the recording + if(sessionModel.jamTracks() && sessionModel.recordingModel.isRecording()) { + logger.debug("preemptive jamtrack stop") + startStopRecording(); + } + + if(!data.endReached) { + logger.debug("calling jamClient.SessionStopPlay. endReached:", data.endReached); + context.jamClient.SessionStopPlay(); + } + } + function onPlay(e, data) { logger.debug("calling jamClient.SessionStartPlay"); context.jamClient.SessionStartPlay(data.playbackMode); @@ -2925,7 +3109,11 @@ logger.debug("calling jamClient.SessionTrackSeekMs(" + seek + ")"); if(data.playbackMonitorMode == context.JK.PLAYBACK_MONITOR_MODE.JAMTRACK) { + // this doesn't ever show anything, because of blocking nature of the seek call + //var $mediaSeeking = $screen.find('.media-seeking') + //$mediaSeeking.attr('data-mode', 'SEEKING') context.jamClient.SessionJamTrackSeekMs(seek); + //$mediaSeeking.attr('data-mode', '') } else { context.jamClient.SessionTrackSeekMs(seek); @@ -3000,7 +3188,6 @@ var mode = data.playbackMode; // will be either 'self' or 'cricket' logger.debug("setting metronome playback mode: ", mode) - var isCricket = mode == 'cricket'; context.jamClient.setMetronomeCricketTestState(isCricket); } @@ -3025,11 +3212,18 @@ return true; } + function showFTUEWhenNoInputs( ) { + //app.afterFtue = function() { window.location.reload }; + //app.layout.startNewFtue(); + window.location = '/client#/account/audio' + } + function events() { $('#session-leave').on('click', sessionLeave); $('#session-resync').on('click', sessionResync); + $('#session-webcam').on('click', sessionWebCam); $('#session-contents').on("click", '[action="delete"]', deleteSession); - $('#tracks').on('click', 'div[control="mute"]', toggleMute); + $tracksHolder.on('click', 'div[control="mute"]', toggleMute); $('#recording-start-stop').on('click', startStopRecording); $('#open-a-recording').on('click', openRecording); $('#open-a-jamtrack').on('click', openJamTrack); @@ -3038,14 +3232,29 @@ $('#session-invite-musicians').on('click', inviteMusicians); $('#session-invite-musicians2').on('click', inviteMusicians); $('#track-settings').click(function() { + + if(gearUtils.isNoInputProfile()) { + // show FTUE + // showFTUEWhenNoInputs(); + app.notify({title:'Settings Disabled', text:'You can not alter any settings for the System Default playback device.'}) + return false; + } + else { configureTrackDialog.refresh(); configureTrackDialog.showVoiceChatPanel(true); configureTrackDialog.showMusicAudioPanel(true); + } }); + $openFtue.click(function() { + showFTUEWhenNoInputs(); + return false; + }) + $closePlaybackRecording.on('click', closeOpenMedia); $(playbackControls) .on('pause', onPause) + .on('stop', onStop) .on('play', onPlay) .on('change-position', onChangePlayPosition); $(friendInput).focus(function() { $(this).val(''); }) @@ -3083,6 +3292,8 @@ $mixModeDropdown = $screen.find('select.monitor-mode') $templateMixerModeChange = $('#template-mixer-mode-change'); $otherAudioContainer = $('#session-recordedtracks-container'); + $myTracksNoTracks = $('#session-mytracks-notracks') + $openFtue = $screen.find('.open-ftue-no-tracks') $myTracksContainer = $('#session-mytracks-container') $liveTracksContainer = $('#session-livetracks-container'); $closePlaybackRecording = $('#close-playback-recording') @@ -3093,8 +3304,13 @@ $myTracks = $screen.find('.session-mytracks'); $liveTracks = $screen.find('.session-livetracks'); $audioTracks = $screen.find('.session-recordings'); - $fluidTracks = $screen.find('.session-fluidtracks') - + $fluidTracks = $screen.find('.session-fluidtracks'); + $voiceChat = $screen.find('#voice-chat'); + $tracksHolder = $screen.find('#tracks') + if(gon.global.video_available && gon.global.video_available!="none") { + webcamViewer.init($(".webcam-container")) + webcamViewer.setVideoOff() + } events(); @@ -3121,6 +3337,20 @@ promptLeave = _promptLeave; } + this.onPlaybackStateChange = function(change_type){ + // if it's play_stop or play_start, poke the playControls + + if(change_type == 'play_start') { + playbackControls.onPlayStartEvent(); + } + else if(change_type == 'play_stop'){ + playbackControls.onPlayStopEvent(); + } + else if(change_type == 'play_pause'){ + playbackControls.onPlayPauseEvent(); + } + } + context.JK.HandleVolumeChangeCallback = handleVolumeChangeCallback; context.JK.HandleMetronomeCallback = handleMetronomeCallback; context.JK.HandleBridgeCallback = handleBridgeCallback; diff --git a/web/app/assets/javascripts/sessionModel.js b/web/app/assets/javascripts/sessionModel.js index 05f726557..cd7edb8ae 100644 --- a/web/app/assets/javascripts/sessionModel.js +++ b/web/app/assets/javascripts/sessionModel.js @@ -12,6 +12,7 @@ var ALERT_TYPES = context.JK.ALERT_TYPES; var EVENTS = context.JK.EVENTS; var MIX_MODES = context.JK.MIX_MODES; + var gearUtils = context.JK.GearUtilsInstance; var userTracks = null; // comes from the backend var clientId = client.clientID; @@ -155,6 +156,7 @@ } // did I open up the current JamTrack? function selfOpenedJamTracks() { + logger.debug("currentSession", currentSession) return currentSession && (currentSession.jam_track_initiator_id == context.JK.currentUserId) } @@ -213,7 +215,9 @@ // see if we already have tracks; if so, we need to run with these var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient); - if(inputTracks.length > 0) { + + logger.debug("isNoInputProfile", gearUtils.isNoInputProfile()) + if(inputTracks.length > 0 || gearUtils.isNoInputProfile() ) { logger.debug("on page enter, tracks are already available") sessionPageEnterDeferred.resolve(inputTracks); var deferred = sessionPageEnterDeferred; @@ -796,6 +800,14 @@ } } + function onPlaybackStateChange(type, text) { + + // if text is play_start or play_stop, tell the play_controls + + if(sessionScreen) { + sessionScreen.onPlaybackStateChange(text); + } + } function onBackendMixerChanged(type, text) { logger.debug("BACKEND_MIXER_CHANGE alert. reason:" + text); @@ -906,6 +918,7 @@ this.onBroadcastStopped = onBroadcastStopped; this.onBroadcastSuccess = onBroadcastSuccess; this.onBroadcastFailure = onBroadcastFailure; + this.onPlaybackStateChange = onPlaybackStateChange; this.getCurrentSession = function() { return currentSession; diff --git a/web/app/assets/javascripts/session_utils.js b/web/app/assets/javascripts/session_utils.js index d1e2358f7..9d54f2493 100644 --- a/web/app/assets/javascripts/session_utils.js +++ b/web/app/assets/javascripts/session_utils.js @@ -10,6 +10,7 @@ var rest = new context.JK.Rest(); context.JK.SessionUtils = sessionUtils; var logger = context.JK.logger; + var autoOpenJamTrack = null; var LATENCY = sessionUtils.LATENCY = { ME : {description: "ME", style: "latency-me", min: -1, max: -1}, @@ -20,6 +21,18 @@ UNKNOWN: {description: "UNKNOWN", style: "latency-unknown", min: -2, max: -2} }; + sessionUtils.setAutoOpenJamTrack = function(jamTrack) { + logger.debug("setting auto-load jamtrack") + autoOpenJamTrack = jamTrack; + } + + // one shot! + sessionUtils.grabAutoOpenJamTrack = function() { + var jamTrack = autoOpenJamTrack; + autoOpenJamTrack = null; + return jamTrack; + } + sessionUtils.createOpenSlot = function($openSlotsTemplate, slot, openSlotCount, currentSlotIndex) { var inst = context.JK.getInstrumentIcon24(slot.instrument_id); @@ -148,6 +161,7 @@ successCallback(); } }); + } sessionUtils.joinSession = function(sessionId) { diff --git a/web/app/assets/javascripts/shopping_cart.js b/web/app/assets/javascripts/shopping_cart.js index 86157b96d..9193d9735 100644 --- a/web/app/assets/javascripts/shopping_cart.js +++ b/web/app/assets/javascripts/shopping_cart.js @@ -11,10 +11,11 @@ var $content = null; function beforeShow(data) { - loadShoppingCarts(); + clearContent(); } function afterShow(data) { + loadShoppingCarts(); } function afterHide() { @@ -29,19 +30,35 @@ function proceedCheckout(e) { e.preventDefault(); - if (!context.JK.currentUserId) { - window.location = '/client#/checkoutSignin'; + if (context.JK.currentUserFreeJamTrack) { + if(context.JK.currentUserId) { + logger.debug("proceeding to redeem complete screen because user has a free jamtrack and is logged in") + window.location = '/client#/redeemComplete' + } + else { + logger.debug("proceeding to redeem signup screen because user has a free jamtrack and is not logged in") + window.location = '/client#/redeemSignup' + } } else { - app.user().done(function(user) { + if (!context.JK.currentUserId) { + logger.debug("proceeding to signin screen because there is no user") + window.location = '/client#/checkoutSignin'; + } + else { + var user = app.currentUser(); + if(user.has_recurly_account && user.reuse_card) { + logger.debug("proceeding to checkout order screen because we have card info already") window.location = '/client#/checkoutOrder'; } else { + logger.debug("proceeding to checkout payment screen because we do not have card info") window.location = '/client#/checkoutPayment'; } - }) + } } + } function removeCart(e) { @@ -55,6 +72,7 @@ .fail(app.ajaxError); } + function clearContent() { $content.empty(); } @@ -69,11 +87,24 @@ function renderShoppingCarts(carts) { var data = {}; - var latest_cart = carts[carts.length - 1]; + + if(carts.length > 0) { + var latest_cart = carts[0]; + } var $latestCartHtml = ""; + var any_in_us = false + context._.each(carts, function(cart) { + if(cart.product_info.sales_region == 'United States') { + any_in_us = true + } + }) + if (latest_cart) { + + latest_cart.any_in_us = any_in_us + $latestCartHtml = $( context._.template( $('#template-shopping-cart-header').html(), @@ -85,9 +116,9 @@ var sub_total = 0; $.each(carts, function(index, cart) { - sub_total += parseFloat(cart.product_info.price) * parseFloat(cart.quantity); + sub_total += parseFloat(cart.product_info.real_price); }); - data.sub_total = sub_total.toFixed(2); + data.sub_total = sub_total; data.carts = carts; var $cartsHtml = $( diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index 33ba2dd4e..cd44c1a76 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -581,6 +581,7 @@ logger.debug("offBackendEvent: " + alertData.name + '.' + namespace, alertData) $(document).off(alertData.name + '.' + namespace); } + /* * Loads a listbox or dropdown with the values in input_array, setting the option value * to the id_field and the option text to text_field. It will preselect the option with @@ -621,9 +622,14 @@ } // returns Fri May 20, 2013 - context.JK.formatDate = function (dateString) { + context.JK.formatDate = function (dateString, suppressDay) { var date = new Date(dateString); - return days[date.getDay()] + ' ' + months[date.getMonth()] + ' ' + context.JK.padString(date.getDate(), 2) + ', ' + date.getFullYear(); + return (suppressDay ? '' : (days[date.getDay()] + ' ')) + months[date.getMonth()] + ' ' + context.JK.padString(date.getDate(), 2) + ', ' + date.getFullYear(); + } + + // returns June for months 0-11 + context.JK.getMonth = function(monthNumber) { + return months[monthNumber]; } context.JK.formatDateYYYYMMDD = function(dateString) { @@ -921,10 +927,7 @@ context.JK.popExternalLinks = function ($parent) { - if(!$parent) $parent = $('body'); - // Allow any a link with a rel="external" attribute to launch - // the link in the default browser, using jamClient: - $parent.on('click', 'a[rel="external"]', function (evt) { + function popOpenBrowser (evt) { if (!context.jamClient) { return; @@ -932,6 +935,7 @@ evt.preventDefault(); var href = $(this).attr("href"); + if (href) { // make absolute if not already if (href.indexOf('http') != 0 && href.indexOf('mailto') != 0) { @@ -941,7 +945,12 @@ context.jamClient.OpenSystemBrowser(href); } return false; - }); + } + + if(!$parent) $parent = $('body'); + // Allow any a link with a rel="external" attribute to launch + // the link in the default browser, using jamClient: + $parent.on('click', 'a[rel="external"]', popOpenBrowser); } context.JK.popExternalLink = function (href) { @@ -1111,7 +1120,7 @@ context.JK.guardAgainstBrowser = function(app, args) { if(!gon.isNativeClient) { - logger.debug("guarding against normal browser on screen thaht requires native client") + logger.debug("guarding against normal browser on screen that requires native client") app.layout.showDialog('launch-app-dialog', args) .one(EVENTS.DIALOG_CLOSED, function() { if(args && args.goHome) { @@ -1124,6 +1133,47 @@ return true; } + + context.JK.createSession = function(app, data) { + + // auto pick an 'other' instrument + var otherId = context.JK.server_to_client_instrument_map.Other.server_id; // get server ID + var otherInstrumentInfo = context.JK.instrument_id_to_instrument[otherId]; // get display name + var beginnerLevel = 1; // default to beginner + var instruments = [ {id: otherId, name: otherInstrumentInfo.display, level: beginnerLevel} ]; + $.each(instruments, function(index, instrument) { + var slot = {}; + slot.instrument_id = instrument.id; + slot.proficiency_level = instrument.level; + slot.approve = true; + data.rsvp_slots.push(slot); + }); + + data.isUnstructuredRsvp = true; + + return rest.createScheduledSession(data) + } + + context.JK.privateSessionSettings = function(createSessionSettings) { + createSessionSettings.genresValues = ['Pop']; + createSessionSettings.genres = ['pop']; + createSessionSettings.timezone = 'Central Time (US & Canada),America/Chicago' + createSessionSettings.name = "Private Test Session"; + createSessionSettings.description = "Private session set up just to test things out in the session interface by myself."; + createSessionSettings.notations = []; + createSessionSettings.language = 'eng' + createSessionSettings.legal_policy = 'Standard'; + createSessionSettings.musician_access = false + createSessionSettings.fan_access = false + createSessionSettings.fan_chat = false + createSessionSettings.approval_required = false + createSessionSettings.legal_terms = true + createSessionSettings.recurring_mode = 'once'; + createSessionSettings.start = new Date().toDateString() + ' ' + context.JK.formatUtcTime(new Date(), false); + createSessionSettings.duration = "60"; + createSessionSettings.open_rsvps = false + createSessionSettings.rsvp_slots = []; + } /* * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message * Digest Algorithm, as defined in RFC 1321. diff --git a/web/app/assets/javascripts/web/affiliate_links.js.coffee b/web/app/assets/javascripts/web/affiliate_links.js.coffee new file mode 100644 index 000000000..be15b3e5a --- /dev/null +++ b/web/app/assets/javascripts/web/affiliate_links.js.coffee @@ -0,0 +1,55 @@ + + +$ = jQuery +context = window +context.JK ||= {}; + +class AffiliateLinks + constructor: (@app, @partner_id) -> + @logger = context.JK.logger + @rest = new context.JK.Rest(); + + initialize: () => + @page = $('body') + @sections = ['jamtrack_songs', 'jamtrack_bands', 'jamtrack_general', 'jamkazam', 'sessions', 'recordings'] + @jamtrack_songs = @page.find('table.jamtrack_songs tbody') + @jamtrack_bands = @page.find('table.jamtrack_bands tbody') + @jamtrack_general = @page.find('table.jamtrack_general tbody') + @jamkazam = @page.find('table.jamkazam tbody') + @sessions= @page.find('table.sessions tbody') + @recordings = @page.find('table.recordings tbody') + + @iterate() + + onGetLinks: (links) => + + $table = @page.find('table.' + @section + ' tbody') + template = $('#template-affiliate-link-row').html(); + context._.each(links, (item) => + $link = $(context._.template(template, item, {variable: 'data'})); + $link.find('td.copy-link a').click(@copyLink) + $table.append($link) + ) + + if @sections.length > 0 + @iterate() + + + copyLink: () -> + $element = $(this) + $url = $element.closest('tr').find('td.url input') + $url.select() + + return false; + + + iterate: () => + @section = @sections.shift() + + @rest.getLinks(@section, @partner_id) + .done(@onGetLinks) + + + +context.JK.AffiliateLinks = AffiliateLinks + diff --git a/web/app/assets/javascripts/web/affiliate_program.js.coffee b/web/app/assets/javascripts/web/affiliate_program.js.coffee new file mode 100644 index 000000000..c013abbf2 --- /dev/null +++ b/web/app/assets/javascripts/web/affiliate_program.js.coffee @@ -0,0 +1,119 @@ + + +$ = jQuery +context = window +context.JK ||= {}; + +class AffiliateProgram + constructor: (@app) -> + @logger = context.JK.logger + @rest = new context.JK.Rest(); + @agreeBtn = null + @disagreeBtn = null + @entityForm = null + @disagreeNotice = null + @entityName = null + @entityType = null + @entityRadio = null + @fieldEntityName = null + @fieldEntityType = null + @entityOptions = null + + removeErrors: () => + @fieldEntityName.removeClass('error').find('.error-info').remove(); + @fieldEntityType.removeClass('error').find('.error-info').remove(); + @entityOptions.removeClass('error').find('.error-info').remove(); + + onRadioChanged: () => + @removeErrors() + value = @page.find('input:radio[name="entity"]:checked').val() + + if value == 'individual' + @entityForm.slideUp() + else + @entityForm.slideDown() + + return false + + + onCreatedAffiliatePartner:(response) => + if response.partner_user_id? + # this was an existing user, so tell them to go on in + context.JK.Banner.show({buttons: [{name: 'GO TO AFFILIATE PAGE', href: '/client#/account/affiliatePartner'}], title: 'congratulations', html: 'Thank you for joining the JamKazam affiliate program!

    You can visit the Affiliate Page in your JamKazam Account any time to get links to share to refer users, and to view reports on affiliate activity levels.'}) + else + context.JK.Banner.show({buttons: [{name: 'GO SIGNUP', href:'/signup?affiliate_partner_id=' + response.id}], title: 'congratulations', html: 'Thank you for joining the JamKazam affiliate program!

    There is still one more step: you still need to create a user account on JamKazam, so that you can access your affiliate information.'}) + + onFailedCreateAffiliatePartner: (jqXHR) => + if jqXHR.status == 422 + body = JSON.parse(jqXHR.responseText) + if body.errors && body.errors.affiliate_partner && body.errors.affiliate_partner[0] == 'You are already an affiliate.' + @app.notify({title:'Error', text:'You are already an affiliate.'}) + else + @app.notifyServerError(jqXHR, 'Unable to Create Affiliate') + else + @app.notifyServerError(jqXHR, 'Unable to Create Affiliate') + + onAgreeClicked: () => + @removeErrors() + + value = @page.find('input:radio[name="entity"]:checked').val() + + error = false + + if value? + if value == 'individual' + entityType = 'Individual' + else + # insist that they fill out entity type info + entityName = @entityName.val() + entityType = @entityType.val() + + entityNameNotEmpty = !!entityName + entityTypeNotEmpty = !!entityType + + if !entityNameNotEmpty + @fieldEntityName.addClass('error').append('
    must be specified
    ') + error = true + + if !entityTypeNotEmpty + @fieldEntityType.addClass('error').append('
    must be specified
    ') + error = true + else + @entityOptions.addClass('error') + error = true + + unless error + @rest.createAffiliatePartner({partner_name: entityName, entity_type: entityType}) + .done(@onCreatedAffiliatePartner) + .fail(@onFailedCreateAffiliatePartner) + + @disagreeNotice.hide ('hidden') + return false + + onDisagreeClicked: () => + @removeErrors() + + @disagreeNotice.slideDown('hidden') + return false + + events:() => + @entityRadio.on('change', @onRadioChanged) + @agreeBtn.on('click', @onAgreeClicked) + @disagreeBtn.on('click', @onDisagreeClicked) + + initialize: () => + @page = $('body') + @agreeBtn = @page.find('.agree-button') + @disagreeBtn = @page.find('.disagree-button') + @entityForm = @page.find('.entity-info') + @disagreeNotice = @page.find('.disagree-text') + @entityName = @page.find('input[name="entity-name"]') + @entityType = @page.find('select[name="entity-type"]') + @entityRadio = @page.find('input[name="entity"]') + @fieldEntityName = @page.find('.field.entity.name') + @fieldEntityType = @page.find('.field.entity.type') + @entityOptions = @page.find('.entity-options') + + @events() + +context.JK.AffiliateProgram = AffiliateProgram \ No newline at end of file diff --git a/web/app/assets/javascripts/web/congratulations.js b/web/app/assets/javascripts/web/congratulations.js index 576759f17..31ddf40d4 100644 --- a/web/app/assets/javascripts/web/congratulations.js +++ b/web/app/assets/javascripts/web/congratulations.js @@ -8,13 +8,6 @@ if(musician) { context.JK.Downloads.listClients(true); } - - if(registrationType) { - $(function() { - // ga() object isn't ready until the page is loaded - context.JK.GA.trackRegister(musician, registrationType); - }); - } } context.congratulations = congratulations; diff --git a/web/app/assets/javascripts/web/home.js b/web/app/assets/javascripts/web/home.js new file mode 100644 index 000000000..5eb7c1ed1 --- /dev/null +++ b/web/app/assets/javascripts/web/home.js @@ -0,0 +1,18 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + + var rest = context.JK.Rest(); + var logger = context.JK.logger; + + function initialize() { + if(gon.signed_in) { + window.location = "/client#/home" + } + } + context.JK.HomePage = initialize; + + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/web/individual_jamtrack.js b/web/app/assets/javascripts/web/individual_jamtrack.js index bbfd05896..ebb720f2c 100644 --- a/web/app/assets/javascripts/web/individual_jamtrack.js +++ b/web/app/assets/javascripts/web/individual_jamtrack.js @@ -9,24 +9,32 @@ var logger = context.JK.logger; var $page = null; var $jamtrack_name = null; + var $jamtrack_band = null; var $previews = null; var $jamTracksButton = null; var $genericHeader = null; var $individualizedHeader = null; + var $ctaJamTracksButton = null; function fetchJamTrack() { - rest.getJamTrack({plan_code: gon.jam_track_plan_code}) + rest.getJamTrackWithArtistInfo({plan_code: gon.jam_track_plan_code}) .done(function (jam_track) { logger.debug("jam_track", jam_track) if(!gon.just_previews) { if (gon.generic) { $genericHeader.removeClass('hidden'); + $jamTracksButton.attr('href', '/client#/jamtrackBrowse') + $jamTracksButton.removeClass('hidden').text("Check out all 100+ JamTracks") + } else { $individualizedHeader.removeClass('hidden') - $jamtrack_name.text(jam_track.name); - $jamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrack') + $jamtrack_name.text('"' + jam_track.name + '"'); + $jamtrack_band.text(jam_track.original_artist) + $jamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrackBrowse') + $jamTracksButton.removeClass('hidden').text("Preview all " + jam_track.band_jam_track_count + " of our " + jam_track.original_artist + " JamTracks") + $ctaJamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrackBrowse') } } @@ -36,7 +44,11 @@ $previews.append($element); - new context.JK.JamTrackPreview(app, $element, jam_track, track, {master_shows_duration: false}) + new context.JK.JamTrackPreview(app, $element, jam_track, track, {master_shows_duration: false, color:'black', master_adds_line_break: true, preload_master:true}) + + if(track.track_type =='Master') { + context.JK.HelpBubbleHelper.rotateJamTrackLandingBubbles($element.find('.jam-track-preview'), $page.find('.video-wrapper'), $page.find('.cta-free-jamtrack a'), $page.find('a.browse-jamtracks')); + } }) $previews.append('
    ') @@ -44,16 +56,19 @@ .fail(function () { app.notify({title: 'Unable to fetch JamTrack', text: "Please refresh the page or try again later."}) }) - } function initialize() { $page = $('body') $jamtrack_name = $page.find('.jamtrack_name') + $jamtrack_band = $page.find('.jamtrack_band') $previews = $page.find('.previews') - $jamTracksButton = $page.find('.browse-jamtracks-wrapper .white-bordered-button') + $jamTracksButton = $page.find('.browse-jamtracks') + $ctaJamTracksButton = $page.find('.cta-free-jamtrack'); $genericHeader = $page.find('h1.generic') $individualizedHeader = $page.find('h1.individualized') + + context.JK.Tracking.adTrack(app) fetchJamTrack(); } diff --git a/web/app/assets/javascripts/web/individual_jamtrack_band.js b/web/app/assets/javascripts/web/individual_jamtrack_band.js index 0de60a2e7..22ceb52a3 100644 --- a/web/app/assets/javascripts/web/individual_jamtrack_band.js +++ b/web/app/assets/javascripts/web/individual_jamtrack_band.js @@ -13,27 +13,32 @@ var $jamTrackNoun = null; var $previews = null; var $jamTracksButton = null; + var $jamtrack_band = null; var $checkItOut = null; + var $ctaJamTracksButton = null; function fetchJamTrack() { rest.getJamTrackWithArtistInfo({plan_code: gon.jam_track_plan_code}) .done(function (jam_track) { logger.debug("jam_track", jam_track) - $jamTrackBandInfo.text(jam_track.band_jam_track_count + ' ' + jam_track.original_artist); - $jamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrack') + $jamtrack_band.text(jam_track.original_artist) + $jamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrackBrowse') + $jamTracksButton.removeClass('hidden').text("Preview all " + jam_track.band_jam_track_count + " of our " + jam_track.original_artist + " JamTracks") + $ctaJamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrackBrowse') + - if(jam_track.band_jam_track_count == 1) { - $jamTrackNoun.text('JamTrack') - $checkItOut.text(', Check It Out!') - } context._.each(jam_track.tracks, function (track) { var $element = $('
    ') $previews.append($element); - new context.JK.JamTrackPreview(app, $element, jam_track, track, {master_shows_duration: false}) + new context.JK.JamTrackPreview(app, $element, jam_track, track, {master_shows_duration: false, color:'black', master_adds_line_break:true, preload_master:true}) + + if(track.track_type =='Master') { + context.JK.HelpBubbleHelper.rotateJamTrackLandingBubbles($element.find('.jam-track-preview'), $page.find('.video-wrapper'), $page.find('.cta-free-jamtrack a'), $page.find('a.browse-jamtracks')); + } }) $previews.append('
    ') @@ -48,10 +53,11 @@ $page = $('body') $jamTrackBandInfo = $page.find('.jamtrack_band_info') $previews = $page.find('.previews') - $jamTracksButton = $page.find('.browse-jamtracks-wrapper .white-bordered-button') - $jamTrackNoun = $page.find('.jamtrack_noun') - $checkItOut = $page.find('.check-it-out') + $jamtrack_band = $page.find('.jamtrack_band') + $jamTracksButton = $page.find('.browse-jamtracks') + $ctaJamTracksButton = $page.find('.cta-free-jamtrack'); + context.JK.Tracking.adTrack(app) fetchJamTrack(); } diff --git a/web/app/assets/javascripts/web/signin_helper.js b/web/app/assets/javascripts/web/signin_helper.js index 76a5dfaab..56bef7160 100644 --- a/web/app/assets/javascripts/web/signin_helper.js +++ b/web/app/assets/javascripts/web/signin_helper.js @@ -20,21 +20,26 @@ var $rememberMe = null; var useAjax = false; var EVENTS = context.JK.EVENTS; - - function reset() { + var argRedirectTo = null; + + function reset(_redirectTo) { + argRedirectTo = _redirectTo; + clear(); + } + + function clear() { $signinForm.removeClass('login-error') $email.val(''); $password.val(''); $rememberMe.attr('checked', 'checked') } - function login() { var email = $email.val(); var password = $password.val(); var rememberMe = $rememberMe.is(':checked') - reset(); + clear(); $signinBtn.text('TRYING...'); @@ -42,6 +47,15 @@ .done(function() { //app.layout.closeDialog('signin-dialog') + if(argRedirectTo) { + if(argRedirectTo instanceof Function) { + argRedirectTo(); + } + else { + window.location.href = argRedirectTo; + } + return; + } var redirectTo = $.QueryString['redirect-to']; if(redirectTo) { logger.debug("redirectTo:" + redirectTo); @@ -68,7 +82,7 @@ function events() { $signinCancelBtn.click(function(e) { - app.layout.closeDialog('signin-dialog'); + app.layout.closeDialog('signin-dialog', true); e.stopPropagation(); return false; }); diff --git a/web/app/assets/javascripts/web/tracking.js.coffee b/web/app/assets/javascripts/web/tracking.js.coffee new file mode 100644 index 000000000..b3fb626f4 --- /dev/null +++ b/web/app/assets/javascripts/web/tracking.js.coffee @@ -0,0 +1,81 @@ + + +$ = jQuery +context = window +context.JK ||= {}; + +class Tracking + constructor: () -> + @logger = context.JK.logger + @rest = new context.JK.Rest(); + + adTrack: (app) => + utmSource = $.QueryString['utm_source'] + if utmSource == 'facebook-ads' || utmSource == 'google-ads' || utmSource == 'twitter-ads' || utmSource == 'affiliate' || utmSource == 'pr' + if !context.jamClient.IsNativeClient() + if context.JK.currentUserId? + app.user().done( (user) => + # relative to 1 day ago (24 * 60 * 60 * 1000) + if new Date(user.created_at).getTime() < new Date().getTime() - 86400000 + @logger.debug("existing user recorded") + context.JK.GA.virtualPageView('/landing/jamtracks/existing-user/'); + else + @logger.debug("new user recorded") + context.JK.GA.virtualPageView('/landing/jamtracks/new-user/') + ) + else + @logger.debug("new user recorded") + context.JK.GA.virtualPageView('/landing/jamtracks/new-user/') + else + @logger.debug("existing user recorded") + context.JK.GA.virtualPageView('/landing/jamtracks/existing-user/'); + + jamtrackBrowseTrack: (app) => + if context.JK.currentUserId? + app.user().done( (user) => + if context.jamClient.IsNativeClient() + @logger.debug("client user recorded") + context.JK.GA.virtualPageView('/client#/jamtrackBrowse/user-in-app') + else + if new Date(user.created_at).getTime() < new Date().getTime() - 86400000 + @logger.debug("existing user recorded") + context.JK.GA.virtualPageView('/client#/jamtrackBrowse/existing-user') + else + @logger.debug("existing new recorded") + context.JK.GA.virtualPageView('/client#/jamtrackBrowse/new-user') + ) + else + if context.jamClient.IsNativeClient() + @logger.debug("client user recorded") + context.JK.GA.virtualPageView('/client#/jamtrackBrowse/user-in-app') + else + @logger.debug("existing new recorded") + context.JK.GA.virtualPageView('/client#/jamtrackBrowse/new-user') + + + + redeemSignupTrack: (app) => + if context.JK.currentUserId? + app.user().done( (user) => + if context.jamClient.IsNativeClient() + @logger.debug("client user recorded") + context.JK.GA.virtualPageView('/client#/redeemSignup/user-in-app') + else + if new Date(user.created_at).getTime() < new Date().getTime() - 86400000 + @logger.debug("existing existing recorded") + context.JK.GA.virtualPageView('/client#/redeemSignup/existing-user') + else + @logger.debug("existing new recorded") + context.JK.GA.virtualPageView('/client#/redeemSignup/new-user') + ) + else + if context.jamClient.IsNativeClient() + @logger.debug("client user recorded") + context.JK.GA.virtualPageView('/client#/redeemSignup/user-in-app') + else + @logger.debug("existing new recorded") + context.JK.GA.virtualPageView('/client#/redeemSignup/new-user') + + +context.JK.Tracking = new Tracking() + diff --git a/web/app/assets/javascripts/web/web.js b/web/app/assets/javascripts/web/web.js index a36ad12e2..89f49d6fe 100644 --- a/web/app/assets/javascripts/web/web.js +++ b/web/app/assets/javascripts/web/web.js @@ -1,3 +1,4 @@ +//= require bugsnag //= require bind-polyfill //= require jquery //= require jquery.monkeypatch @@ -20,6 +21,7 @@ //= require jquery.icheck //= require jquery.bt //= require jquery.exists +//= require jquery.visible //= require howler.core.js //= require AAA_Log //= require AAC_underscore @@ -47,8 +49,8 @@ //= require subscription_utils //= require ui_helper //= require custom_controls -//= require ga //= require jam_rest +//= require ga //= require session_utils //= require recording_utils //= require helpBubbleHelper @@ -61,9 +63,12 @@ //= require web/sessions //= require web/session_info //= require web/recordings -//= require web/welcome +//= require web/home +//= require web/tracking //= require web/individual_jamtrack //= require web/individual_jamtrack_band +//= require web/affiliate_program +//= require web/affiliate_links //= require fakeJamClient //= require fakeJamClientMessages //= require fakeJamClientRecordings diff --git a/web/app/assets/javascripts/webcam_viewer.js.coffee b/web/app/assets/javascripts/webcam_viewer.js.coffee new file mode 100644 index 000000000..ed2b1d772 --- /dev/null +++ b/web/app/assets/javascripts/webcam_viewer.js.coffee @@ -0,0 +1,115 @@ +$ = jQuery +context = window +context.JK ||= {}; + +context.JK.WebcamViewer = class WebcamViewer + constructor: (@root) -> + @client = context.jamClient + @logger = context.JK.logger + @initialScan = false + @toggleBtn = null + @webcamSelect = null + @resolutionSelect = null + @videoShared=false + @resolution=null + + init: (root) => + @root = root + @toggleBtn = @root.find(".webcam-test-btn") + @webcamSelect = @root.find(".webcam-select-container select") + @resolutionSelect = @root.find(".webcam-resolution-select-container select") + @webcamSelect.on("change", this.selectWebcam) + @toggleBtn.on 'click', @toggleWebcam + + beforeShow:() => + this.loadWebCams() + this.selectWebcam() + this.loadResolutions() + this.selectResolution() + @initialScan = true + @client.SessStopVideoSharing() + #client.SessSetInsetPosition(5) + #client.SessSetInsetSize(1) + #client.FTUESetAutoSelectVideoLayout(false) + #client.SessSelectVideoDisplayLayoutGroup(1) + + + selectWebcam:(e, data) => + device = @webcamSelect.val() + if device? + caps = @client.FTUEGetVideoCaptureDeviceCapabilities(device) + @logger.debug("Got capabilities from device", caps, device) + @client.FTUESelectVideoCaptureDevice(device, caps) + + selectResolution:() => + @logger.debug 'Selecting res control: ', @resolutionSelect + @resolution = @resolutionSelect.val() + if @resolution? + @logger.debug 'Selecting res: ', @resolution + @client.FTUESetVideoEncodeResolution @resolution + # if @isVideoShared + # this.setVideoOff() + # this.toggleWebcam() + + setVideoOff:() => + if this.isVideoShared() + @client.SessStopVideoSharing() + + isVideoShared:() => + @videoShared + + setToggleState:() => + available = @webcamSelect.find('option').size() > 0 + shared = this.isVideoShared() + @toggleBtn.prop 'disabled', true + @toggleBtn.prop 'disabled', !available + + toggleWebcam:() => + @logger.debug 'Toggling webcam from: ', this.isVideoShared() + if this.isVideoShared() + @toggleBtn.removeClass("selected") + @client.SessStopVideoSharing() + @videoShared = false + else + @toggleBtn.addClass("selected") + @client.SessStartVideoSharing 0 + @videoShared = true + + selectedDeviceName:() => + webcamName="None Configured" + webcam = @client.FTUECurrentSelectedVideoDevice() + if (webcam? && Object.keys(webcam).length>0) + webcamName = _.values(webcam)[0] + + webcamName + + loadWebCams:() => + devices = @client.FTUEGetVideoCaptureDeviceNames() + selectedDevice = this.selectedDeviceName() + selectControl = @webcamSelect + context._.each devices, (device) -> + selected = device == selectedDevice + option = $('