diff --git a/admin/.rakeTasks b/admin/.rakeTasks index c6865d9a1..78308c2e6 100644 --- a/admin/.rakeTasks +++ b/admin/.rakeTasks @@ -4,4 +4,4 @@ You are allowed to: 1. Remove rake task 2. Add existing rake tasks To add existing rake tasks automatically delete this file and reload the project. ---> +--> diff --git a/admin/Gemfile b/admin/Gemfile index 09ad1d8f6..e941f5518 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -27,6 +27,7 @@ gem 'coffee-rails' #, '~> 3.2.1' # See https://github.com/sstephenson/execjs#readme for more supported runtimes # gem 'therubyracer', :platforms => :ruby +gem 'kickbox' gem 'uglifier' #, '>= 1.0.3' # this version is pinned due to this: https://github.com/gregbell/active_admin/issues/1939 @@ -71,9 +72,13 @@ gem 'iso-639' gem 'rubyzip' gem 'sanitize' gem 'slim' -gem 'influxdb', '0.1.8' -gem 'influxdb-rails', '0.1.10' +#gem 'influxdb', '0.1.8' +#gem 'influxdb-rails', '0.1.10' gem 'recurly' +gem 'sendgrid_toolkit', '>= 1.1.1' +gem 'stripe' +gem 'zip-codes' +gem 'email_validator' group :libv8 do gem 'libv8', "~> 4.5.95" @@ -100,7 +105,6 @@ end group :development, :test do gem 'capybara' gem 'rspec-rails', '2.14.2' - gem 'guard-rspec' gem 'jasmine', '1.3.1' gem 'execjs', '1.4.0' #gem 'therubyracer' #, '0.11.0beta8' diff --git a/admin/Rakefile b/admin/Rakefile index 3065cf5e9..d88365d80 100644 --- a/admin/Rakefile +++ b/admin/Rakefile @@ -1,3 +1,4 @@ + #!/usr/bin/env rake # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. diff --git a/admin/app/admin/affiliate_quarterly_totals.rb b/admin/app/admin/affiliate_quarterly_totals.rb new file mode 100644 index 000000000..4f065c267 --- /dev/null +++ b/admin/app/admin/affiliate_quarterly_totals.rb @@ -0,0 +1,37 @@ +ActiveAdmin.register JamRuby::AffiliateQuarterlyPayment, :as => 'Affiliate Quarterly Payments' do + + menu :label => 'Quarterly Reports', :parent => 'Affiliates' + + config.sort_order = 'due_amount_in_cents DESC' + config.batch_actions = false + config.clear_action_items! + config.filters = true + config.per_page = 50 + config.paginate = true + + filter :affiliate_partner + filter :year + filter :quarter + filter :closed + filter :paid + + form :partial => 'form' + + index do + + # default_actions # use this for all view/edit/delete links + + column 'Year' do |oo| oo.year end + column 'Quarter' do |oo| oo.quarter end + column 'Partner' do |oo| link_to(oo.affiliate_partner.display_name, oo.affiliate_partner.admin_url, {:title => oo.affiliate_partner.display_name}) end + column "Due (\u00A2)" do |oo| oo.due_amount_in_cents end + column 'JamTracks Sold' do |oo| oo.jamtracks_sold end + column 'Paid' do |oo| oo.paid end + column 'Closed' do |oo| oo.paid end + + end + + controller do + + end +end diff --git a/admin/app/admin/affiliate_traffic_totals.rb b/admin/app/admin/affiliate_traffic_totals.rb new file mode 100644 index 000000000..f3f24b8ab --- /dev/null +++ b/admin/app/admin/affiliate_traffic_totals.rb @@ -0,0 +1,36 @@ +ActiveAdmin.register JamRuby::AffiliateTrafficTotal, :as => 'Affiliate Daily Stats' do + + menu :label => 'Daily Stats', :parent => 'Affiliates' + + config.sort_order = 'referral_user_count DESC' + config.batch_actions = false + config.clear_action_items! + config.filters = true + config.per_page = 50 + config.paginate = true + + filter :affiliate_partner + filter :day + filter :signups + filter :visits + + form :partial => 'form' + + scope("Active", default: true) { |scope| scope.where('visits != 0 or signups != 0').order('day desc') } + + index do + + # default_actions # use this for all view/edit/delete links + + column 'Day' do |oo| oo.day end + column 'Partner' do |oo| link_to(oo.affiliate_partner.display_name, oo.affiliate_partner.admin_url, {:title => oo.affiliate_partner.display_name}) end + column 'Signups' do |oo| oo.signups end + column 'Visits' do |oo| oo.visits end + + end + + + controller do + + end +end diff --git a/admin/app/admin/affiliate_users.rb b/admin/app/admin/affiliate_users.rb index b21d512eb..1658d926d 100644 --- a/admin/app/admin/affiliate_users.rb +++ b/admin/app/admin/affiliate_users.rb @@ -2,15 +2,18 @@ ActiveAdmin.register JamRuby::User, :as => 'Referrals' do menu :label => 'Referrals', :parent => 'Affiliates' + config.sort_order = 'created_at DESC' config.batch_actions = false config.clear_action_items! - config.filters = false + config.filters = true + + filter :affiliate_referral index do - column 'User' do |oo| link_to(oo.name, "http://www.jamkazam.com/client#/profile/#{oo.id}", {:title => oo.name}) end + column 'User' do |oo| link_to(oo.name, oo.admin_url, {:title => oo.name}) end column 'Email' do |oo| oo.email end column 'Created' do |oo| oo.created_at end - column 'Partner' do |oo| oo.affiliate_referral.partner_name end + column 'Partner' do |oo| oo.affiliate_referral.display_name end end controller do diff --git a/admin/app/admin/affiliates.rb b/admin/app/admin/affiliates.rb index 46a7d0cc4..2ff8813e3 100644 --- a/admin/app/admin/affiliates.rb +++ b/admin/app/admin/affiliates.rb @@ -6,10 +6,12 @@ ActiveAdmin.register JamRuby::AffiliatePartner, :as => 'Affiliates' do config.batch_actions = false # config.clear_action_items! config.filters = false + config.per_page = 50 + config.paginate = true form :partial => 'form' - scope("Active", default: true) { |scope| scope.where('partner_user_id IS NOT NULL') } + scope("Active", default: true) { |scope| scope.where('partner_user_id IS NOT NULL').order('referral_user_count desc') } scope("Unpaid") { |partner| partner.unpaid } index do diff --git a/admin/app/admin/campaign_spends.rb b/admin/app/admin/campaign_spends.rb new file mode 100644 index 000000000..038672855 --- /dev/null +++ b/admin/app/admin/campaign_spends.rb @@ -0,0 +1,87 @@ +ActiveAdmin.register_page "CampaignSpend" do + menu :parent => 'JamClass' + + page_action :create_spend, :method => :post do + + campaign = params[:jam_ruby_campaign_spend][:campaign] + year = params[:jam_ruby_campaign_spend][:year] + month = params[:jam_ruby_campaign_spend][:month] + spend = params[:jam_ruby_campaign_spend][:spend] + + if campaign.blank? + redirect_to admin_campaignspend_path, :notice => "No campaign defined! Nothing done." + return + elsif spend.blank? + redirect_to admin_campaignspend_path, :notice => "No spend defined! Nothing done." + return + elsif year.blank? || month.blank? + spend = spend.to_f + # get all cohorts for a given campaign + campaign_cohorts = JamClassReport.where(campaign: campaign).where("cohort IS NOT NULL") + year_months = [] + campaign_cohorts.each do |cohort| + year_month = {year: cohort.cohort.year, month: cohort.cohort.month} + year_months << year_month + end + + if campaign_cohorts.length > 0 + per_month = spend / campaign_cohorts.length + year_months.each do |year_month| + campaign_spend = CampaignSpend.where(campaign: campaign).where(year: year_month[:year]).where(month: year_month[:month]).first + + if campaign_spend.nil? + campaign_spend = CampaignSpend.new + end + campaign_spend.campaign = campaign + campaign_spend.month = year_month[:month] + campaign_spend.year = year_month[:year] + campaign_spend.spend = per_month + + campaign_spend.save! + + end + else + redirect_to admin_campaignspend_path, :notice => "No data found for campaign: #{campaign}" + return + end + + + redirect_to admin_campaignspend_path, :notice => "Campaign #{campaign} updated with a per month value of $#{per_month} (#{year_months.length} months worth of data found)" + else + campaign_spend = CampaignSpend.where(campaign: campaign).where(year: year).where(month: month).first + + if campaign_spend.nil? + campaign_spend = CampaignSpend.new + end + campaign_spend.campaign = campaign + campaign_spend.month = month + campaign_spend.year = year + campaign_spend.spend = spend + + campaign_spend.save! + + redirect_to admin_campaignspend_path, :notice => "Campaign spend updated: #{campaign}:#{year}-#{month} = $#{spend}" + end + + end + + + content do + + para do + link_to "JamClass Report", admin_jamclassreports_path + end + para do + semantic_form_for CampaignSpend.new, :url => admin_campaignspend_create_spend_path, :builder => ActiveAdmin::FormBuilder do |f| + f.inputs "Campaign Spend" do + f.input :spend, :required => true, hint: "If you leave year or month blank, the system will divide up the specified spend amount here across all months seen for this campaign." + f.input :campaign, :as => :select, hint: "If this appears empty or incomplete, visit the JamClass Report page (link above) and come back.", :required => true, :collection => JamClassReport.select('campaign').group('campaign').map(&:campaign) + f.input :year, :as => :select, :hint => "Year of campaign spend (optional)", :collection => [Date.today.year, Date.today.year - 1] + f.input :month, :as => :select, :hint => "Month of campaign (optional)", :collection => (1..12).map { |m| [Date::MONTHNAMES[m], m] } + end + f.actions + end + end + + end +end diff --git a/admin/app/admin/chat_messages.rb b/admin/app/admin/chat_messages.rb new file mode 100644 index 000000000..3353786ba --- /dev/null +++ b/admin/app/admin/chat_messages.rb @@ -0,0 +1,29 @@ +ActiveAdmin.register JamRuby::ChatMessage, :as => 'ChatMessage' do + # Note: a lame thing is it's not obvious how to make it search on email instead of user_id. + filter :music_session_id + filter :user_id + + menu :parent => 'Misc' + + config.per_page = 200 + + config.sort_order = 'created_at DESC' + + scope("Global", default:true) { |scope| + scope.where("channel = 'global'") + } + scope("Session", ) { |scope| + scope.where("channel = 'session'") + } + index do + column 'User' do |oo| link_to(oo.user.email, oo.user.admin_url, {:title => oo.user.email}) end + column "Timestamp" do |post| + (post.created_at).strftime('%b %d %Y, %H:%M') + end + column "Message" do |post| + post.message + end + actions + + end +end diff --git a/admin/app/admin/crash_dumps.rb b/admin/app/admin/crash_dumps.rb index bcfcd9106..4d85ae8ae 100644 --- a/admin/app/admin/crash_dumps.rb +++ b/admin/app/admin/crash_dumps.rb @@ -1,28 +1,27 @@ ActiveAdmin.register JamRuby::CrashDump, :as => 'Crash Dump' do # Note: a lame thing is it's not obvious how to make it search on email instead of user_id. filter :timestamp - filter :user_email, :as => :string filter :client_id + filter :user_id + menu :parent => 'Misc' + config.sort_order = 'created_at DESC' + index do + column 'User' do |oo| oo.user ? link_to(oo.user.email, oo.user.admin_url, {:title => oo.user.email}) : '' end + column "Client Version", :client_version + column "Client Type", :client_type + column "Download" do |post| + link_to 'Link', post.sign_url + end column "Timestamp" do |post| (post.timestamp || post.created_at).strftime('%b %d %Y, %H:%M') end - column "Client Type", :client_type - column "Dump URL" do |post| - link_to post.uri, post.uri + column "Description" do |post| + post.description end - - column "User ID", :user_id - - # FIXME (?): This isn't performant (though it likely doesn't matter). Could probably do a join. - column "User Email" do |post| - unless post.user_id.nil? - post.user_email - end - end - column "Client ID", :client_id actions + end end diff --git a/admin/app/admin/download_tracker.rb b/admin/app/admin/download_tracker.rb new file mode 100644 index 000000000..6d0daa013 --- /dev/null +++ b/admin/app/admin/download_tracker.rb @@ -0,0 +1,35 @@ +ActiveAdmin.register JamRuby::DownloadTracker, :as => 'DownloadTrackers' do + + menu :label => 'Download Trackers', :parent => 'JamTracks' + + config.batch_actions = false + config.filters = true + config.per_page = 50 + + filter :remote_ip + + index do + column 'User' do |oo| oo.user ? link_to(oo.user.email, oo.user.admin_url, {:title => oo.user.email}) : '' end + column 'Created' do |oo| oo.created_at end + column 'JamTrack' do |oo| oo.jam_track end + column 'Paid' do |oo| oo.paid end + column 'Blacklisted?' do |oo| IpBlacklist.listed(oo.remote_ip) ? 'Yes' : 'No' end + column 'Remote IP' do |oo| oo.remote_ip end + column "" do |oo| + link_to 'Blacklist This IP', "download_trackers/#{oo.id}/blacklist_by_ip" + end + end + + member_action :blacklist_by_ip, :method => :get do + tracker = DownloadTracker.find(params[:id]) + + if !IpBlacklist.listed(tracker.remote_ip) + ip = IpBlacklist.new + ip.remote_ip = tracker.remote_ip + ip.save! + end + + redirect_to admin_download_trackers_path, :notice => "IP address #{tracker.remote_ip} blacklisted." + end + +end diff --git a/admin/app/admin/email_blacklist.rb b/admin/app/admin/email_blacklist.rb new file mode 100644 index 000000000..615ff43fb --- /dev/null +++ b/admin/app/admin/email_blacklist.rb @@ -0,0 +1,14 @@ +ActiveAdmin.register JamRuby::EmailBlacklist, :as => 'Email Blacklist' do + + menu :label => 'Email Blacklist', :parent => 'Operations' + + config.sort_order = 'created_at desc' + config.batch_actions = false + + index do + column :email + column :source + column :notes + column :created_at + end +end \ No newline at end of file diff --git a/admin/app/admin/fake_purchaser.rb b/admin/app/admin/fake_purchaser.rb index 5ed5c9ef2..e318413ac 100644 --- a/admin/app/admin/fake_purchaser.rb +++ b/admin/app/admin/fake_purchaser.rb @@ -32,6 +32,7 @@ ActiveAdmin.register_page "Fake Purchaser" do jam_track_right.user = user jam_track_right.jam_track = jam_track jam_track_right.is_test_purchase = true + jam_track_right.version = jam_track.version jam_track_right.save! count = count + 1 end diff --git a/admin/app/admin/gift_card_upload.rb b/admin/app/admin/gift_card_upload.rb new file mode 100644 index 000000000..87e69850e --- /dev/null +++ b/admin/app/admin/gift_card_upload.rb @@ -0,0 +1,41 @@ +ActiveAdmin.register_page "Giftcarduploads" do + + menu :label => 'Gift Cards Upload', :parent => 'JamTracks' + + page_action :upload_giftcards, :method => :post do + GiftCard.transaction do + + puts params + + file = params[:jam_ruby_gift_card][:csv] + array_of_arrays = CSV.read(file.tempfile.path) + array_of_arrays.each do |row| + if row.length != 1 + raise "UKNONWN CSV FORMAT! Must be 1 column" + end + + code = row[0] + + gift_card = GiftCard.new + gift_card.code = code + gift_card.card_type = params[:jam_ruby_gift_card][:card_type] + gift_card.origin = file .original_filename + gift_card.save! + end + + redirect_to admin_giftcarduploads_path, :notice => "Created #{array_of_arrays.length} gift cards!" + end + end + + content do + semantic_form_for GiftCard.new, :url => admin_giftcarduploads_upload_giftcards_path, :builder => ActiveAdmin::FormBuilder do |f| + f.inputs "Upload Gift Cards" do + f.input :csv, as: :file, required: true, :label => "A single column CSV that contains ONE type of gift card (5 JamTrack, 10 JamTrack, etc)" + f.input :card_type, required:true, as: :select, :collection => JamRuby::GiftCard::CARD_TYPES + end + f.actions + end + end + +end + diff --git a/admin/app/admin/gift_cards.rb b/admin/app/admin/gift_cards.rb new file mode 100644 index 000000000..8e1d4e80d --- /dev/null +++ b/admin/app/admin/gift_cards.rb @@ -0,0 +1,24 @@ +ActiveAdmin.register JamRuby::GiftCard, :as => 'GiftCards' do + + menu :label => 'Gift Cards', :parent => 'JamTracks' + + config.batch_actions = false + config.filters = true + config.per_page = 50 + + scope("Redeemed Most Recently", default: true) { |scope| scope.where('user_id IS NOT NULL').order('updated_at DESC') } + scope("Available") { |scope| scope.where('user_id is NULL') } + + filter :card_type + filter :origin + filter :code + + index do + column 'User' do |oo| oo.user ? link_to(oo.user.email, oo.user.admin_url, {:title => oo.user.email}) : '' end + column 'Code' do |oo| oo.code end + column 'Card Type' do |oo| oo.card_type end + column 'Origin' do |oo| oo.origin end + column 'Created' do |oo| oo.created_at end + end + +end diff --git a/admin/app/admin/interested_schools.rb b/admin/app/admin/interested_schools.rb new file mode 100644 index 000000000..fb3a1abe0 --- /dev/null +++ b/admin/app/admin/interested_schools.rb @@ -0,0 +1,20 @@ +ActiveAdmin.register JamRuby::User, :as => 'SchoolInterest' do + + menu :label => 'Interested in Schools', :parent => 'JamClass' + + config.sort_order = 'created_at desc' + config.batch_actions = false + config.per_page = 100 + config.paginate = true + config.filters = false + + scope("All", default: true) { |scope| scope.where(school_interest: true) } + + index do + column "Name" do |user| + span do + link_to "#{user.name} (#{user.email})", "#{Rails.application.config.external_root_url}/client#/profile/#{user.id}" + end + end + end +end \ No newline at end of file diff --git a/admin/app/admin/ip_blacklist.rb b/admin/app/admin/ip_blacklist.rb new file mode 100644 index 000000000..3a8f1e14e --- /dev/null +++ b/admin/app/admin/ip_blacklist.rb @@ -0,0 +1,13 @@ +ActiveAdmin.register JamRuby::IpBlacklist, :as => 'IP Blacklist' do + + menu :label => 'IP Blacklist', :parent => 'Operations' + + config.sort_order = 'created_at desc' + config.batch_actions = false + + index do + column :remote_ip + column :notes + column :created_at + end +end \ No newline at end of file diff --git a/admin/app/admin/ip_whitelist.rb b/admin/app/admin/ip_whitelist.rb new file mode 100644 index 000000000..e5e53b228 --- /dev/null +++ b/admin/app/admin/ip_whitelist.rb @@ -0,0 +1,13 @@ +ActiveAdmin.register JamRuby::IpWhitelist, :as => 'IP Whitelist' do + + menu :label => 'IP Whitelist', :parent => 'Operations' + + config.sort_order = 'created_at desc' + config.batch_actions = false + + index do + column :remote_ip + column :notes + column :created_at + end +end \ No newline at end of file diff --git a/admin/app/admin/jam_class_knobs.rb b/admin/app/admin/jam_class_knobs.rb new file mode 100644 index 000000000..f27e3f0e0 --- /dev/null +++ b/admin/app/admin/jam_class_knobs.rb @@ -0,0 +1,14 @@ +ActiveAdmin.register_page "Jam Class Knobs" do + menu :parent => 'JamClass' + + page_action :force_hourly, :method => :post do + + Resque.enqueue(HourlyJob) + redirect_to admin_jam_class_knobs_path, :notice => "Re-running the Hourly Job. Lessons will be analysed; any payments will be attempted that should be, etc" + end + + + action_item do + link_to "Force Hourly Background Job", admin_jam_class_knobs_force_hourly_path, :method => :post + end +end \ No newline at end of file diff --git a/admin/app/admin/jam_class_report.rb b/admin/app/admin/jam_class_report.rb new file mode 100644 index 000000000..28b32f811 --- /dev/null +++ b/admin/app/admin/jam_class_report.rb @@ -0,0 +1,66 @@ +ActiveAdmin.register_page "JamClassReports", as: "JamClass Cohort Report" do + menu :parent => 'JamClass' + + content :title => "JamClass Report" do + para do + link_to "Campaign Spend", admin_campaignspend_path + end + para do + table_for JamClassReport.analyse do + + column "Campaign" do |r| + if r.campaign.nil? + "N/A" + else + r.campaign + end + end + column "Cohort" do |r| + if r.cohort.nil? + "Total" + else + "#{Date::ABBR_MONTHNAMES[r.cohort.month]} #{r.cohort.year}" + end + end + column "Spend" do |r| + if r.spend.nil? + "N/A" + else + r.spend + end + end + column "Registrations", :registrations + column "TD Customers", :td_customers + column "JamClass Revenues", :jamclass_rev + column "TD4", :td4 + column "TD2", :td2 + column "TD1", :td1 + column "Spend/TD" do |r| + if r.spend_td.nil? + "N/A" + else + r.spend_td + end + end + column "% 0 BC" do |r| + (r.purchases0 * 100).round + end + column "% 1 BC" do |r| + (r.purchases1 * 100).round + end + column "% 2 BC" do |r| + (r.purchases2 * 100).round + end + column "% 3 BC" do |r| + (r.purchases3 * 100).round + end + column "% 4+ BC" do |r| + (r.purchases_rest * 100).round + end + end + end + + end +end + + diff --git a/admin/app/admin/jam_tracks.rb b/admin/app/admin/jam_tracks.rb index 7e5fc6090..0a306db1b 100644 --- a/admin/app/admin/jam_tracks.rb +++ b/admin/app/admin/jam_tracks.rb @@ -5,13 +5,17 @@ ActiveAdmin.register JamRuby::JamTrack, :as => 'JamTracks' do config.sort_order = 'name_asc' config.batch_actions = false + filter :name + filter :original_artist filter :genres filter :status, :as => :select, collection: JamRuby::JamTrack::STATUS scope("Default", default: true) { |scope| scope } scope("Onboarding TODO") { |scope| scope.where('onboarding_exceptions is not null') } scope("Tency Only") { |scope| scope.joins('INNER JOIN jam_track_licensors as licensors ON jam_tracks.licensor_id = licensors.id').where("licensors.name = 'Tency Music'") } - scope("Onboarding TODO w/ Tency Only") { |scope| scope.joins('INNER JOIN jam_track_licensors as licensors ON jam_tracks.licensor_id = licensors.id').where("licensors.name = 'Tency Music'").where('onboarding_exceptions is not null') } + scope("TimTracks Only") { |scope| scope.joins('INNER JOIN jam_track_licensors as licensors ON jam_tracks.licensor_id = licensors.id').where("licensors.name = 'Tim Waurick'") } + scope("Drumma Boy Only") { |scope| scope.joins('INNER JOIN jam_track_licensors as licensors ON jam_tracks.licensor_id = licensors.id').where("licensors.name = 'Drumma Boy'") } + # scope("Onboarding TODO w/ Tency Only") { |scope| scope.joins('INNER JOIN jam_track_licensors as licensors ON jam_tracks.licensor_id = licensors.id').where("licensors.name = 'Tency Music'").where('onboarding_exceptions is not null') } form :partial => 'form' diff --git a/admin/app/admin/jamblaster.rb b/admin/app/admin/jamblaster.rb new file mode 100644 index 000000000..d64666711 --- /dev/null +++ b/admin/app/admin/jamblaster.rb @@ -0,0 +1,15 @@ +ActiveAdmin.register JamRuby::Jamblaster, :as => 'Jamblaster' do + + + menu :label => 'JamBlasters', :parent => 'JamBlaster' + + form do |f| + f.inputs 'New JamBlaster' do + f.input :user, required: true, collection: User.all, include_blank: false + f.input :serial_no, required: true + f.input :client_id, required: false + f.input :users, required: true, collection: User.all, include_blank: false + end + f.actions + end +end diff --git a/admin/app/admin/lesson_booking.rb b/admin/app/admin/lesson_booking.rb new file mode 100644 index 000000000..41be9745f --- /dev/null +++ b/admin/app/admin/lesson_booking.rb @@ -0,0 +1,47 @@ +ActiveAdmin.register JamRuby::LessonBooking, :as => 'LessonBookings' do + + menu :label => 'Lesson Booking', :parent => 'JamClass' + + config.sort_order = 'created_at desc' + config.batch_actions = false + config.per_page = 100 + config.paginate = true + config.filters = false + + scope("All", default: true ) { |scope| scope.unscoped.order('created_at desc') } + scope("Requested") { |scope| scope.unscoped.where(status: LessonBooking::STATUS_REQUESTED).order('created_at desc') } + scope("Approved") { |scope| scope.unscoped.approved.order('created_at desc') } + scope("Suspended" ) { |scope| scope.unscoped.suspended.order('created_at desc') } + scope("Canceled" ) { |scope| scope.unscoped.canceled.order('created_at desc') } + + index do + column "User Link" do |lesson_booking| + span do + link_to "Web URL", "#{Rails.application.config.external_root_url}/client#/jamclass/lesson-booking/#{lesson_booking.id}" + end + end + column "Type" do |lesson_booking| + lesson_booking.display_type + end + column "Status" do |lesson_booking| + lesson_booking.status + end + column "Teacher" do |lesson_booking| + teacher = lesson_booking.teacher + span do + link_to "#{teacher.name} (#{teacher.email})", "#{Rails.application.config.external_root_url}/client#/profile/teacher/#{teacher.id}" + end + end + column "Student" do |lesson_booking| + student = lesson_booking.student + span do + link_to "#{student.name} (#{student.email})", "#{Rails.application.config.external_root_url}/client#/profile/#{student.id}" + end + end + end + + show do + + end + +end \ No newline at end of file diff --git a/admin/app/admin/lesson_session.rb b/admin/app/admin/lesson_session.rb new file mode 100644 index 000000000..cd4458a43 --- /dev/null +++ b/admin/app/admin/lesson_session.rb @@ -0,0 +1,129 @@ +ActiveAdmin.register JamRuby::LessonSession, :as => 'LessonSessions' do + + menu :label => 'Lesson Session', :parent => 'JamClass' + + config.sort_order = 'created_at desc' + config.batch_actions = false + config.per_page = 100 + config.paginate = true + config.filters = false + + scope("All", default: true) { |scope| scope.unscoped.order('created_at desc') } + scope("Requested") { |scope| scope.unscoped.where(status: LessonBooking::STATUS_REQUESTED).order('created_at desc') } + scope("Approved") { |scope| scope.unscoped.approved.order('created_at desc') } + scope("Suspended") { |scope| scope.unscoped.suspended.order('created_at desc') } + scope("Canceled") { |scope| scope.unscoped.canceled.order('created_at desc') } + scope("Missed") { |scope| scope.unscoped.missed.order('created_at desc') } + scope("Completed") { |scope| scope.unscoped.completed.order('created_at desc') } + + index do + column "User Link" do |lesson_session| + lesson_booking = lesson_session.lesson_booking + span do + link_to "Web URL", "#{Rails.application.config.external_root_url}/client#/jamclass/lesson-booking/#{lesson_booking.id}" + end + end + column "Status" do |lesson_session| + link_to lesson_session.status, admin_lesson_session_path(lesson_session.id) + end + column "Start Time" do |lesson_session| + span do + if lesson_session.music_session.nil? + raise "Lessonsesison with no id #{lesson_session.id}" + else + lesson_session.music_session.pretty_scheduled_start(true) + end + end + br + span do + lesson_session.music_session.scheduled_start + end + end + column "Duration" do |lesson_session| + lesson_session.duration + end + column "Teacher" do |lesson_session| + teacher = lesson_session.teacher + span do + link_to "#{teacher.name} (#{teacher.email})", "#{Rails.application.config.external_root_url}/client#/profile/teacher/#{teacher.id}" + end + end + column "Student" do |lesson_session| + student = lesson_session.student + span do + link_to "#{student.name} (#{student.email})", "#{Rails.application.config.external_root_url}/client#/profile/#{student.id}" + end + end + end + + show do + attributes_table do + row "User Link" do |lesson_session| + lesson_booking = lesson_session.lesson_booking + span do + link_to "Web URL", "#{Rails.application.config.external_root_url}/client#/jamclass/lesson-booking/#{lesson_booking.id}" + end + end + row "Status" do |lesson_session| + lesson_session.status + end + row "Start Time" do |lesson_session| + span do + lesson_session.music_session.pretty_scheduled_start(true) + end + br + span do + lesson_session.music_session.scheduled_start + end + end + row "Duration" do |lesson_session| + lesson_session.duration + end + row "Teacher" do |lesson_session| + teacher = lesson_session.teacher + span do + link_to "#{teacher.name} (#{teacher.email})", "#{Rails.application.config.external_root_url}/client#/profile/teacher/#{teacher.id}" + end + end + row "Student" do |lesson_session| + student = lesson_session.student + span do + link_to "#{student.name} (#{student.email})", "#{Rails.application.config.external_root_url}/client#/profile/#{student.id}" + end + end + row "Followup Emails Sent" do |lesson_session| + span do + lesson_session.sent_notices + end + end + row "Success" do |lesson_session| + span do + lesson_session.success + end + end + row "Billed" do |lesson_session| + span do + lesson_session.billed + end + end + row "Description" do |lesson_session| + span do + lesson_session.timed_description + end + end + row "Analysis" do |lesson_session| + if lesson_session.analysed + span style: "white-space: pre;" do + begin + JSON.pretty_generate(lesson_session.analysis_json) + rescue + "barf" + end + end + end + end + end + + end + +end \ No newline at end of file diff --git a/admin/app/admin/monthly_stats.rb b/admin/app/admin/monthly_stats.rb new file mode 100644 index 000000000..0a8e609bb --- /dev/null +++ b/admin/app/admin/monthly_stats.rb @@ -0,0 +1,38 @@ +ActiveAdmin.register_page "Monthly Stats" do + menu :parent => 'Reports' + + + content :title => "Monthly Stats" do + h2 "Distinct Users Playing in Sessions" + table_for MusicSession.select([:month, :count]).find_by_sql("select date_trunc('month', msuh.created_at)::date as month, count(distinct(user_id)) from music_sessions_user_history msuh group by month order by month desc;") do + column "Month", Proc.new { |row| Date.parse(row.month).strftime('%B %Y') } + column "Users", :count + end + + h2 "Music Sessions" + table_for MusicSession.select([:month, :count]).find_by_sql("select date_trunc('month', ms.created_at)::date as month, count(id) from music_sessions ms where started_at is not null group by month order by month desc;") do + column "Month", Proc.new { |row| Date.parse(row.month).strftime('%B %Y') } + column "Sessions", :count + end + + h2 "Distinct Users Who Played with a JamTrack" + table_for MusicSession.select([:month, :count]).find_by_sql("select date_trunc('month', jts.created_at)::date as month, count(distinct(user_id)) from jam_track_sessions jts group by month order by month desc;") do + column "Month", Proc.new { |row| Date.parse(row.month).strftime('%B %Y') } + column "Users", :count + end + + h2 "Music Sessions with JamTracks Played" + table_for MusicSession.select([:month, :count]).find_by_sql("select date_trunc('month', jts.created_at)::date as month, count(distinct(music_session_id)) from jam_track_sessions jts where session_type = 'session' group by month order by month desc;") do + column "Month", Proc.new { |row| Date.parse(row.month).strftime('%B %Y') } + column "Sessions", :count + end + + h2 "JamTrack Web Player Sessions" + table_for MusicSession.select([:month, :count]).find_by_sql("select date_trunc('month', jts.created_at)::date as month, count(id) from jam_track_sessions jts where session_type = 'browser' group by month order by month desc;") do + column "Month", Proc.new { |row| Date.parse(row.month).strftime('%B %Y') } + column "Sessions", :count + end + + end + +end \ No newline at end of file diff --git a/admin/app/admin/news.rb b/admin/app/admin/news.rb new file mode 100644 index 000000000..996783c7a --- /dev/null +++ b/admin/app/admin/news.rb @@ -0,0 +1,6 @@ +ActiveAdmin.register JamRuby::News, :as => 'News' do + + menu :parent => 'Misc' + + +end diff --git a/admin/app/admin/sale_line_items.rb b/admin/app/admin/sale_line_items.rb new file mode 100644 index 000000000..fa90ffc4e --- /dev/null +++ b/admin/app/admin/sale_line_items.rb @@ -0,0 +1,43 @@ +ActiveAdmin.register JamRuby::SaleLineItem, :as => 'Sale Line Items' do + + menu :label => 'Line Items', :parent => 'Purchases' + + config.sort_order = 'created_at DESC' + config.batch_actions = false + config.clear_action_items! + config.filters = true + config.per_page = 50 + config.paginate = true + + filter :affiliate_referral_id + filter :free + + form :partial => 'form' + + scope("Non Free", default: true) { |scope| scope.where(free: false).order('created_at desc') } + scope("Free") { |scope| scope.where(free: true).order('created_at desc') } + + index do + # default_actions # use this for all view/edit/delete links + + column 'Product' do |oo| oo.product end + column "Partner" do |oo| + link_to("#{oo.affiliate_referral.display_name} #{oo.affiliate_referral_fee_in_cents ? "#{oo.affiliate_referral_fee_in_cents}\u00A2" : ''}", oo.affiliate_referral.admin_url, {:title => oo.affiliate_referral.display_name}) if oo.affiliate_referral + end + column 'User' do |oo| + link_to(oo.sale.user.name, admin_user_path(oo.sale.user.id), {:title => oo.sale.user.name}) + end + column 'Source' do |oo| + oo.sale.source + end + column 'When' do |oo| + oo.created_at + end + + end + + + controller do + + end +end diff --git a/admin/app/admin/slow_responses.rb b/admin/app/admin/slow_responses.rb new file mode 100644 index 000000000..46bb3f919 --- /dev/null +++ b/admin/app/admin/slow_responses.rb @@ -0,0 +1,50 @@ +ActiveAdmin.register JamRuby::LessonSession, :as => 'SlowResponses' do + + menu :label => 'Slow Responses', :parent => 'JamClass' + + config.sort_order = 'created_at desc' + config.batch_actions = false + config.per_page = 100 + config.paginate = true + config.filters = false + + scope("Slow Responses", default: true) { |scope| scope.unscoped.slow_responses } + scope("Least Time Left") { |scope| scope.unscoped.least_time_left } + + index do + column "Teacher" do |lesson_session| + teacher = lesson_session.teacher + span do + link_to "#{teacher.name} (#{teacher.email})", "#{Rails.application.config.external_root_url}/client#/profile/teacher/#{teacher.id}" + end + end + column "Student" do |lesson_session| + student = lesson_session.student + span do + link_to "#{student.name} (#{student.email})", "#{Rails.application.config.external_root_url}/client#/profile/#{student.id}" + end + end + column "Type" do |lesson_session| + link_to lesson_session.display_type + end + column "Start Time" do |lesson_session| + span do + if lesson_session.music_session.nil? + raise "Lessonsesison with no id #{lesson_session.id}" + else + lesson_session.music_session.pretty_scheduled_start(true) + end + end + end + column "Last student comms date" do |lesson_session| + lesson_session.last_student_comm_date + end + column "Days with no response" do |lesson_session| + "#{lesson_session.days_no_response} days" + end + + + + end + +end \ No newline at end of file diff --git a/admin/app/admin/students.rb b/admin/app/admin/students.rb new file mode 100644 index 000000000..75c099455 --- /dev/null +++ b/admin/app/admin/students.rb @@ -0,0 +1,60 @@ +ActiveAdmin.register JamRuby::User, :as => 'Students' do + + menu :label => 'Students', :parent => 'JamClass' + + config.sort_order = 'created_at desc' + config.batch_actions = false + config.per_page = 100 + config.paginate = true + config.filters = false + + def booked_anything(scope) + scope.joins(:student_lesson_bookings).where('lesson_bookings.active = true').uniq + end + + scope("Default", default: true) { |scope| scope.where('is_a_student = true OR ((select count(id) from lesson_bookings where lesson_bookings.user_id = users.id) > 0)').order('users.ready_for_session_at IS NULL DESC') } + + index do + column "Name" do |user| + link_to user.name, "#{Rails.application.config.external_root_url}/client#/profile/#{user.id}" + end + column "Email" do |user| + user.email + end + column "Location" do |user| + user.location(country = true) + end + + column "Session Ready" do |user| + div do + if user.ready_for_session_at + span do + 'YES' + end + else + span do + 'NO' + end + span do + br + end + span do + link_to("mark as checked", mark_session_ready_admin_student_path(user.id), {confirm: "Mark as ready for session?"}) + end + + end + end + end + column "School" do |user| + if user.school + user.school.name + end + end + end + + member_action :mark_session_ready, :method => :get do + resource.mark_session_ready + redirect_to :back + end + +end \ No newline at end of file diff --git a/admin/app/admin/teachers.rb b/admin/app/admin/teachers.rb new file mode 100644 index 000000000..e54718565 --- /dev/null +++ b/admin/app/admin/teachers.rb @@ -0,0 +1,288 @@ +ActiveAdmin.register JamRuby::Teacher, :as => 'Teachers' do + + menu :label => 'Teachers', :parent => 'JamClass' + + config.sort_order = 'created_at desc' + config.batch_actions = false + config.per_page = 100 + config.paginate = true + config.filters = false + + scope("All", default: true) { |scope| scope.unscoped.order("background_check_at > '#{(Date.today - 365).to_s}}' NULLS FIRST, ready_for_session_at IS NULL DESC") } + scope("All Sorted By Sign Up") { |scope| scope.unscoped.order("teachers.created_at DESC, background_check_at > '#{(Date.today - 365).to_s}}' NULLS FIRST, ready_for_session_at IS NULL DESC") } + scope("50% and Session Ready" ) { |scope| scope.unscoped.where('profile_pct >= ?', 50.0).where('ready_for_session_at IS NOT NULL').order("background_check_at > '#{(Date.today - 365).to_s}}' NULLS FIRST, ready_for_session_at IS NULL DESC") } + scope("50% and Not Session Ready" ) { |scope| scope.unscoped.where('profile_pct > ?', 50.0).where('ready_for_session_at IS NULL').order("background_check_at > '#{(Date.today - 365).to_s}}' NULLS FIRST, ready_for_session_at IS NULL DESC") } + + index do + column "Name" do |teacher| + span do + link_to "#{teacher.user.name} (#{teacher.user.email})", "#{Rails.application.config.external_root_url}/client#/profile/teacher/#{teacher.user.id}" + end + end + column "Instruments & Genres" do |teacher| + div do + div do + teacher.instruments.join(', ') + end + br + div do + teacher.genres.join(', ') + end + end + end + column "Location" do |teacher| + teacher.user.location(country = true) + end + column "Profile %" do |teacher| + div do + span do + "#{teacher.pct_complete[:pct]}%" + end + br + span do + link_to "Detail", admin_teacher_path(teacher.id) + end + end + + end +=begin + column "Background Check" do |teacher| + div do + if teacher.background_check_at + span do + teacher.background_check_at.to_date + end + span do + br + end + span do + link_to(mark_background_check_admin_teacher_path(teacher.id), {confirm: "Mark as background checked?"}) do + "mark as checked" + end + end + + else + span do + '' + end + span do + br + end + span do + link_to("mark as checked", mark_background_check_admin_teacher_path(teacher.id), {confirm: "Mark as background checked?"}) + end + end + end + end +=end + column "Session Ready" do |teacher| + div do + if teacher.ready_for_session_at + span do + 'YES' + end + else + span do + 'NO' + end + span do + br + end + span do + link_to("mark as checked", mark_session_ready_admin_teacher_path(teacher.id), {confirm: "Mark as ready for session?"}) + end + + end + end + end + column "Top Teacher" do |teacher| + div do + if teacher.top_rated + span do + 'YES' + end + span do + br + end + span do + link_to("mark not top", mark_not_top_admin_teacher_path(teacher.id), {confirm: "Mark as not top rated?"}) + end + else + span do + 'NO' + end + span do + br + end + span do + link_to("mark as top", mark_top_admin_teacher_path(teacher.id), {confirm: "Mark as top rated?"}) + end + end + end + + end + column "Signed Up" do |teacher| + teacher.created_at.to_date + end + column "School" do |teacher| + if teacher.school + teacher.school.name + end + end + end + + show do + attributes_table do + row "Name" do |teacher| + link_to teacher.user.name, "#{Rails.application.config.external_root_url}/client#/profile/teacher/#{teacher.user.id}" + end + row "Email" do |teacher| + teacher.user.email + end + row "Location" do |teacher| + teacher.user.location(country = true) + end + row "Profile %" do |teacher| + div do + span do + "#{teacher.pct_complete[:pct]}%" + end + br + br + div do + h5 do "Completed Sections" end + teacher.pct_complete.each do |k, v| + if k != :pct && v + div do + k + end + end + end + br + br + h5 do "Uncompleted Sections" end + teacher.pct_complete.each do |k, v| + if k != :pct && !v + div do + k + end + end + end + end + end + + end +=begin + row "Background Check" do |teacher| + div do + if teacher.background_check_at + span do + teacher.background_check_at.to_date + end + span do + br + end + span do + link_to(mark_background_check_admin_teacher_path(teacher.id), {confirm: "Mark as background checked?"}) do + "mark as checked" + end + end + + else + span do + 'NOT DONE' + end + span do + br + end + span do + link_to("mark as checked", mark_background_check_admin_teacher_path(teacher.id), {confirm: "Mark as background checked?"}) + end + end + + + end + end +=end + + row "Session Ready" do |teacher| + div do + if teacher.ready_for_session_at + span do + 'YES' + end + else + span do + 'NO' + end + span do + br + end + span do + link_to("mark as checked", mark_session_ready_admin_teacher_path(teacher.id), {confirm: "Mark as ready for session?"}) + end + + end + end + end + row "Top Teacher" do |teacher| + div do + if teacher.top_rated + span do + 'YES' + end + span do + br + end + span do + link_to("mark not top", mark_not_top_admin_teacher_path(teacher.id), {confirm: "Mark as not top rated?"}) + end + else + span do + 'NO' + end + span do + br + end + span do + link_to("mark as top", mark_top_admin_teacher_path(teacher.id), {confirm: "Mark as top rated?"}) + end + end + end + + end + + row "Signed Up" do |teacher| + teacher.created_at.to_date + end + + row "School" do |teacher| + if teacher.school + teacher.school.name + end + end + + end + end + + + member_action :mark_background_check, :method => :get do + resource.mark_background_checked + redirect_to :back + end + + member_action :mark_session_ready, :method => :get do + resource.mark_session_ready + redirect_to :back + end + + member_action :mark_top, :method => :get do + resource.mark_top_rated + redirect_to :back + end + + member_action :mark_not_top, :method => :get do + resource.mark_not_top_rated + redirect_to :back + end +end \ No newline at end of file diff --git a/admin/app/admin/test_drive_packages.rb b/admin/app/admin/test_drive_packages.rb new file mode 100644 index 000000000..ebd4c2268 --- /dev/null +++ b/admin/app/admin/test_drive_packages.rb @@ -0,0 +1,102 @@ +ActiveAdmin.register JamRuby::TestDrivePackage, :as => 'TestDrivePackage' do + + menu :label => 'Test Drive Packages', :parent => 'JamClass' + + config.sort_order = 'created_at desc' + config.batch_actions = false + config.per_page = 100 + config.paginate = true + config.filters = false + + form :partial => 'form' + + + controller do +=begin + def new + test_drive_package = TestDrivePackage.new + t = TestDrivePackageTeacher.new + t.test_drive_package = test_drive_package + test_drive_package.test_drive_package_teachers << t + + t = TestDrivePackageTeacher.new + t.test_drive_package = test_drive_package + test_drive_package.test_drive_package_teachers << t + + @test_drive_package = test_drive_package + puts "OK #{test_drive_package.test_drive_package_teachers.length}" + super + end +=end + + + # def create + # puts params.inspect + # hash = params[:jam_ruby_test_drive_package] + # package = TestDrivePackage.new + # package.name = hash[:name] + # package.package_type = hash[:package_type] + # package.description = hash[:description] + # if package.save + # redirect_to admin_test_drive_package_path(package.id) + # else + # redirect_to admin_test_drive_packages_path, :notice => "Unable to create package. Error: #{package.errors.first[1][0]}" + # end + #end + end + index do + + column "Package ID" do |package| + link_to package.name, "#{APP_CONFIG.external_root_url}/landing/jamclass/students?utm-teachers=#{package.name}" + end + + column "Package Type" do |package| + package.package_type + end + + column "Teachers" do |package| + package.test_drive_package_teachers.each do |package_teacher| + + span do + teacher = package_teacher.user + link_to "#{teacher.name} (#{teacher.email})", "#{Rails.application.config.external_root_url}/client#/profile/teacher/#{teacher.id}" + end + br + end + end + + column "Detail" do |package| + link_to "Detail", admin_test_drive_package_path(package.id) + end + + end + + show do + + attributes_table do + row "Package ID" do |package| + link_to package.name, "#{APP_CONFIG.external_root_url}/landing/jamclass/students?utm-teachers=#{package.name}" + end + + row "Package Type" do |package| + package.package_type + end + + row "Teachers" do |package| + package.test_drive_package_teachers.each do |package_teacher| + + span do + teacher = package_teacher.user + link_to "#{teacher.name} (#{teacher.email})", "#{Rails.application.config.external_root_url}/client#/profile/teacher/#{teacher.id}" + end + br + end + end + + row "Description" do |package| + package.description + end + end + end + +end \ No newline at end of file diff --git a/admin/app/admin/top_selling_jamtracks.rb b/admin/app/admin/top_selling_jamtracks.rb new file mode 100644 index 000000000..b4dbf06d4 --- /dev/null +++ b/admin/app/admin/top_selling_jamtracks.rb @@ -0,0 +1,20 @@ +ActiveAdmin.register_page "Top Selling JamTracks" do + menu :parent => 'Reports' + + top_selling_jamtracks = JamTrack.select([:id, :original_artist, :name, :has_tap_in]).find_by_sql("select count(jam_tracks.id) as count , jam_tracks.id, original_artist, name, (jmep_text is not null) has_tap_in from jam_tracks inner join jam_track_rights on (jam_track_rights.jam_track_id = jam_tracks.id AND jam_track_rights.is_test_purchase = false) group by jam_tracks.id order by count(jam_tracks.id) desc ") + + content :title => "Top Selling JamTracks" do + table_for top_selling_jamtracks do + column "Count", :count + column "Original Artist", :original_artist + column "Name", :name + column "Has Count-in", :has_tap_in + + column "ID", :id + + end + + end + + +end \ No newline at end of file diff --git a/admin/app/admin/user_blacklist.rb b/admin/app/admin/user_blacklist.rb new file mode 100644 index 000000000..538125600 --- /dev/null +++ b/admin/app/admin/user_blacklist.rb @@ -0,0 +1,13 @@ +ActiveAdmin.register JamRuby::UserBlacklist, :as => 'User Blacklist' do + + menu :label => 'User Blacklist', :parent => 'Operations' + + config.sort_order = 'created_at desc' + config.batch_actions = false + + index do + column :user + column :notes + column :created_at + end +end \ No newline at end of file diff --git a/admin/app/admin/user_progression.rb b/admin/app/admin/user_progression.rb index 6ea4bb285..aa72d8014 100644 --- a/admin/app/admin/user_progression.rb +++ b/admin/app/admin/user_progression.rb @@ -9,7 +9,7 @@ ActiveAdmin.register JamRuby::User, :as => 'User Progression' do config.filters = false index do - column :email do |user| link_to(truncate(user.email, {:length => 12}), resource_path(user), {:title => "#{user.first_name} #{user.last_name} (#{user.email})"}) end + column :email do |user| link_to(truncate(user.email, {:length => 12}), resource_path(user), {:title => "#{user.name} (#{user.email})"}) end column :updated_at do |uu| uu.updated_at.strftime(PROGRESSION_DATE) end column :created_at do |uu| uu.created_at.strftime(PROGRESSION_DATE) end column :city diff --git a/admin/app/admin/user_source.rb b/admin/app/admin/user_source.rb new file mode 100644 index 000000000..3e6c6556d --- /dev/null +++ b/admin/app/admin/user_source.rb @@ -0,0 +1,32 @@ +ActiveAdmin.register JamRuby::User, :as => 'UserSource' do + + menu :label => 'User Campaigns', :parent => 'Users' + + config.sort_order = 'created_at DESC' + config.batch_actions = false + config.clear_action_items! + config.filters = false + + scope("Most Recent First", default: true) { |scope| scope.unscoped.order('created_at desc')} + + index do + column "Email" do |user| + user.email + end + column "Bought TestDrive" do |user| + !user.most_recent_test_drive_purchase.nil? ? "Yes" : "No" + end + column "UTM Source" do |user| + user.origin_utm_source + end + column "UTM Medium" do |user| + user.origin_utm_medium + end + column "UTM Campaign" do |user| + user.origin_utm_campaign + end + column "Referrer" do |user| + user.origin_referrer + end + end +end diff --git a/admin/app/admin/user_whitelist.rb b/admin/app/admin/user_whitelist.rb new file mode 100644 index 000000000..16fc8790a --- /dev/null +++ b/admin/app/admin/user_whitelist.rb @@ -0,0 +1,13 @@ +ActiveAdmin.register JamRuby::UserWhitelist, :as => 'User Whitelist' do + + menu :label => 'User Whitelist', :parent => 'Operations' + + config.sort_order = 'created_at desc' + config.batch_actions = false + + index do + column :user + column :notes + column :created_at + end +end \ No newline at end of file diff --git a/admin/app/controllers/application_controller.rb b/admin/app/controllers/application_controller.rb index a28952746..d3e00deb6 100644 --- a/admin/app/controllers/application_controller.rb +++ b/admin/app/controllers/application_controller.rb @@ -1,9 +1,13 @@ class ApplicationController < ActionController::Base + include ApplicationHelper + protect_from_forgery before_filter :prepare_gon def prepare_gon + @olark_enabled = false + gon.olark_box_start_hidden = false gon.prefix = ENV['RAILS_RELATIVE_URL_ROOT'] || '/' end end diff --git a/admin/app/controllers/email_controller.rb b/admin/app/controllers/email_controller.rb index 1e0c55182..9644dfca1 100644 --- a/admin/app/controllers/email_controller.rb +++ b/admin/app/controllers/email_controller.rb @@ -15,5 +15,25 @@ class EmailController < ApplicationController headers['Content-Type'] ||= 'text/csv' @users = User.where(subscribe_email: true) + + # if specified, return only users that have redeemed or bought a JamTrack + if params[:any_jam_track] + @users = @users.select('DISTINCT users.id, email, first_name, last_name').joins(:sales => :sale_line_items).where("sale_line_items.product_type = 'JamTrack'") + end + end + + def dump_teachers + + if params[:code] != Rails.application.config.email_dump_code + render :text => "", :status => 404 + return + end + + headers['Content-Disposition'] = "attachment; filename=\"teacher-list.csv\"" + headers['Content-Type'] ||= 'text/csv' + + @users = User.joins(:teacher) + + render "dump_emailables.csv.erb" end end \ No newline at end of file diff --git a/admin/app/controllers/jam_track_controller.rb b/admin/app/controllers/jam_track_controller.rb index a0e504577..c84d0606a 100644 --- a/admin/app/controllers/jam_track_controller.rb +++ b/admin/app/controllers/jam_track_controller.rb @@ -12,4 +12,19 @@ class JamTrackController < ApplicationController render "jam_track/dump_released", :layout => nil end + def dump_top_selling + + if params[:code] != Rails.application.config.data_dump_code + render :text => "", :status => 404 + return + end + + @jam_tracks = JamTrack.select([:id, :original_artist, :name]).find_by_sql("select count(jam_tracks.id) as count , jam_tracks.id, original_artist, name, (jmep_text is not null) has_tap_in from jam_tracks inner join jam_track_rights on (jam_track_rights.jam_track_id = jam_tracks.id AND jam_track_rights.is_test_purchase = false) group by jam_tracks.id order by count(jam_tracks.id) desc ") + + headers['Content-Disposition'] = "attachment; filename=\"top-selling-jam-tracks.csv\"" + headers['Content-Type'] ||= 'text/csv' + + render "jam_track/dump_top_selling", :layout => nil + + end end \ No newline at end of file diff --git a/admin/app/helpers/application_helper.rb b/admin/app/helpers/application_helper.rb index 6e9385e59..80731359d 100644 --- a/admin/app/helpers/application_helper.rb +++ b/admin/app/helpers/application_helper.rb @@ -1,4 +1,5 @@ module ApplicationHelper + end diff --git a/admin/app/views/admin/affiliates/_form.html.erb b/admin/app/views/admin/affiliates/_form.html.erb index b7a3b047d..78a1dce70 100644 --- a/admin/app/views/admin/affiliates/_form.html.erb +++ b/admin/app/views/admin/affiliates/_form.html.erb @@ -2,6 +2,8 @@ <%= f.semantic_errors *f.object.errors.keys %> <%= f.inputs do %> <%= f.input(:partner_name, :input_html => {:maxlength => 128}) %> + <%= f.input(:entity_type, :as => :select, :collection => AffiliatePartner::ENTITY_TYPES) %> + <%= f.input(:rate) %> <% end %> <%= f.actions %> <% end %> diff --git a/admin/app/views/admin/jam_tracks/_form.html.slim b/admin/app/views/admin/jam_tracks/_form.html.slim index 334cdc27e..ac038e5ca 100644 --- a/admin/app/views/admin/jam_tracks/_form.html.slim +++ b/admin/app/views/admin/jam_tracks/_form.html.slim @@ -13,6 +13,7 @@ = f.input :publisher, :input_html => { :rows=>1, :maxlength=>1000 } = f.input :licensor, collection: JamRuby::JamTrackLicensor.all, include_blank: true = f.input :genres + = f.input :year = f.input :duration, hint: 'this should rarely need editing because it comes from the import process' = f.input :sales_region, collection: JamRuby::JamTrack::SALES_REGION, include_blank: false = f.input :price, :required => true, :input_html => {type: 'numeric'} diff --git a/admin/app/views/admin/test_drive_packages/_form.html.erb b/admin/app/views/admin/test_drive_packages/_form.html.erb new file mode 100644 index 000000000..65d00ccb1 --- /dev/null +++ b/admin/app/views/admin/test_drive_packages/_form.html.erb @@ -0,0 +1,20 @@ +<%= semantic_form_for([:admin, @test_drive_package], html: {:multipart => true}, url: @test_drive_package.new_record? ? admin_test_drive_packages_path : "#{ENV['RAILS_RELATIVE_URL_ROOT']}/admin/test_drive_packages/#{@test_drive_package.id}") do |f| %> + <%= f.semantic_errors *f.object.errors.keys %> + <%= f.inputs do %> + <%= f.input :name, required: true, label: "Package ID", hint: "The name to use as the utm-teachers parameter in the student landing URL" %> + <%= f.input :package_type, required: true, label: "Package Type", as: :select, collection: ['1', '2', '4'], hint: "TestDrive package type" %> + <%= f.input :description, label: "Package Description", hint: "Admin-only friendly description of the package" %> + + <%= f.semantic_fields_for :test_drive_package_teachers do |teacher| %> + <%= render 'test_drive_package_teacher_fields', f: teacher %> + <% end %> + + <% if !@test_drive_package.new_record? %> + + <% end %> + + <%= f.actions %> + <% end %> +<% end %> \ No newline at end of file diff --git a/admin/app/views/admin/test_drive_packages/_test_drive_package_teacher_fields.html.slim b/admin/app/views/admin/test_drive_packages/_test_drive_package_teacher_fields.html.slim new file mode 100644 index 000000000..fdb47b3ba --- /dev/null +++ b/admin/app/views/admin/test_drive_packages/_test_drive_package_teacher_fields.html.slim @@ -0,0 +1,8 @@ += f.inputs name: 'Teachers' do + + ol.nested-fields + //= f.input :test_drive_package, :required=>true, value: @test_drive_package, include_blank: true + = f.input :user, :required=>true, collection: User.where(is_a_teacher: true, phantom: false), include_blank: true + = f.input :short_bio_temp + + = link_to_remove_association "Delete Teacher", f, class: 'button', style: 'margin-left:10px' diff --git a/admin/app/views/email/dump_emailables.csv.erb b/admin/app/views/email/dump_emailables.csv.erb index efa44b0f8..f226e68fa 100644 --- a/admin/app/views/email/dump_emailables.csv.erb +++ b/admin/app/views/email/dump_emailables.csv.erb @@ -1,2 +1,2 @@ <%- headers = ['email', 'name', 'unsubscribe_token'] -%> -<%= CSV.generate_line headers %><%- @users.each do |user| -%><%= CSV.generate_line([user.email, user.first_name, user.unsubscribe_token]) %><%- end -%> \ No newline at end of file +<%= CSV.generate_line headers %><%- @users.each do |user| -%><%= CSV.generate_line([user.email, user.anonymous? ? '-' : user.first_name, user.unsubscribe_token]) %><%- end -%> \ No newline at end of file diff --git a/admin/app/views/jam_track/dump_top_selling.html.erb b/admin/app/views/jam_track/dump_top_selling.html.erb new file mode 100644 index 000000000..a8d59eab0 --- /dev/null +++ b/admin/app/views/jam_track/dump_top_selling.html.erb @@ -0,0 +1,2 @@ +<%- headers = ['Artist Name', 'Song Name', 'ID', 'Count', 'Has Count-in'] -%> +<%= CSV.generate_line headers %><%- @jam_tracks.each do |jam_track| -%><%= CSV.generate_line([jam_track.original_artist, jam_track.name, jam_track.id, jam_track['count'], jam_track['has_tap_in']]) %><%- end -%> \ No newline at end of file diff --git a/admin/config/application.rb b/admin/config/application.rb index 8b349a913..04ac0ef94 100644 --- a/admin/config/application.rb +++ b/admin/config/application.rb @@ -111,6 +111,7 @@ module JamAdmin config.redis_host = "localhost:6379" + config.email_social_alias = 'social@jamkazam.com' config.email_alerts_alias = 'alerts@jamkazam.com' # should be used for 'oh no' server down/service down sorts of emails config.email_generic_from = 'nobody@jamkazam.com' config.email_smtp_address = 'smtp.sendgrid.net' @@ -154,5 +155,13 @@ module JamAdmin config.jmep_dir = ENV['JMEP_DIR'] || File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "jmep")) config.email_dump_code = 'rcAUyC3TZCbgGx4YQpznBRbNnQMXW5iKTzf9NSBfzMLsnw9dRQ' + config.data_dump_code = 'rcAUyC3TZCbgGx4Y3321eudbNnQMXW5iKTzf9NSBfzMLsnw9dRQ' + + config.admin_port = ENV['ADMIN_PORT'] || 3333 + config.admin_root_url = "#{config.external_protocol}#{config.external_hostname}#{(config.admin_port == 80 || config.admin_port == 443) ? '' : ':' + config.admin_port.to_s}" + + config.download_tracker_day_range = 30 + config.max_user_ip_address = 10 + config.max_multiple_users_same_ip = 2 end end diff --git a/admin/config/environments/development.rb b/admin/config/environments/development.rb index 59bfa2e8a..7950d3194 100644 --- a/admin/config/environments/development.rb +++ b/admin/config/environments/development.rb @@ -43,4 +43,5 @@ JamAdmin::Application.configure do config.email_generic_from = 'nobody-dev@jamkazam.com' config.email_alerts_alias = 'alerts-dev@jamkazam.com' + config.email_social_alias = 'social-dev@jamkazam.com' end diff --git a/admin/config/initializers/gift_cards.rb b/admin/config/initializers/gift_cards.rb new file mode 100644 index 000000000..8c967ccb4 --- /dev/null +++ b/admin/config/initializers/gift_cards.rb @@ -0,0 +1,9 @@ +class JamRuby::GiftCard + + attr_accessor :csv + + + def process_csv + + end +end diff --git a/admin/config/initializers/jam_ruby_teacher.rb b/admin/config/initializers/jam_ruby_teacher.rb new file mode 100644 index 000000000..f855ae238 --- /dev/null +++ b/admin/config/initializers/jam_ruby_teacher.rb @@ -0,0 +1,5 @@ + class JamRuby::Teacher + + attr_accessible :short_bio, as: :admin + +end diff --git a/admin/config/initializers/jam_ruby_user.rb b/admin/config/initializers/jam_ruby_user.rb index 57435d83f..5effb5420 100644 --- a/admin/config/initializers/jam_ruby_user.rb +++ b/admin/config/initializers/jam_ruby_user.rb @@ -1,6 +1,8 @@ class JamRuby::User - attr_accessible :admin, :raw_password, :musician, :can_invite, :photo_url, :session_settings, :confirm_url, :email_template # :invite_email + attr_accessible :admin, :raw_password, :musician, :can_invite, :photo_url, :session_settings, :confirm_url, :teacher_attributes, :email_template # :invite_email + + accepts_nested_attributes_for :teacher, allow_destroy: true def raw_password '' diff --git a/admin/config/initializers/jam_tracks.rb b/admin/config/initializers/jam_tracks.rb index 33efd995c..60537c467 100644 --- a/admin/config/initializers/jam_tracks.rb +++ b/admin/config/initializers/jam_tracks.rb @@ -2,28 +2,5 @@ class JamRuby::JamTrack # add a custom validation - attr_accessor :preview_generate_error - before_save :jmep_json_generate - validate :jmep_text_validate - - def jmep_text_validate - begin - JmepManager.execute(self.jmep_text) - rescue ArgumentError => err - errors.add(:jmep_text, err.to_s) - end - end - - def jmep_json_generate - self.licensor_id = nil if self.licensor_id == '' - self.jmep_json = nil if self.jmep_json == '' - self.time_signature = nil if self.time_signature == '' - - begin - self[:jmep_json] = JmepManager.execute(self.jmep_text) - rescue ArgumentError => err - #errors.add(:jmep_text, err.to_s) - end - end end diff --git a/admin/config/routes.rb b/admin/config/routes.rb index 2bbf1b3fa..7c671b018 100644 --- a/admin/config/routes.rb +++ b/admin/config/routes.rb @@ -30,6 +30,8 @@ JamAdmin::Application.routes.draw do match '/api/checks/latency_tester' => 'checks#check_latency_tester', :via => :get match '/api/users/emailables/:code' => 'email#dump_emailables', :via => :get + match '/api/teachers/:code' => 'email#dump_teachers', :via => :get + match '/jam_tracks/top/:code' => 'jam_track#dump_top_selling', :via => :get match '/api/jam_tracks/released' => 'jam_track#dump_released', :via => :get, as: 'released_jamtracks_csv' mount Resque::Server.new, :at => "/resque" diff --git a/db/Gemfile b/db/Gemfile index 79ec5bca4..1908dd61f 100644 --- a/db/Gemfile +++ b/db/Gemfile @@ -3,4 +3,4 @@ source 'http://rubygems.org' # Assumes you have already cloned pg_migrate_ruby in your workspace # $ cd [workspace] # $ git clone https://github.com/sethcall/pg_migrate_ruby -gem 'pg_migrate', '0.1.13', :source => 'http://rubygems.org/' +gem 'pg_migrate', '0.1.14', :source => 'http://rubygems.org/' diff --git a/db/Gemfile.lock b/db/Gemfile.lock index 88080fb81..649f3aaaf 100644 --- a/db/Gemfile.lock +++ b/db/Gemfile.lock @@ -1,21 +1,21 @@ GEM remote: http://rubygems.org/ specs: - little-plugger (1.1.3) + little-plugger (1.1.4) logging (1.7.2) little-plugger (>= 1.1.3) pg (0.17.1) - pg_migrate (0.1.13) + pg_migrate (0.1.14) logging (= 1.7.2) pg (= 0.17.1) thor - thor (0.18.1) + thor (0.19.1) PLATFORMS ruby DEPENDENCIES - pg_migrate (= 0.1.13)! + pg_migrate (= 0.1.14)! BUNDLED WITH - 1.10.5 + 1.11.2 diff --git a/db/manifest b/db/manifest index 9f84ea0a2..0130037cb 100755 --- a/db/manifest +++ b/db/manifest @@ -303,4 +303,61 @@ jam_track_name_drop_unique.sql jam_track_searchability.sql harry_fox_agency.sql jam_track_slug.sql -rails4_migration.sql \ No newline at end of file +mixdown.sql +aac_master.sql +video_recording.sql +web_playable_jamtracks.sql +affiliate_partner_rate.sql +track_downloads.sql +jam_track_lang_idx.sql +giftcard.sql +add_description_to_crash_dumps.sql +acappella.sql +purchasable_gift_cards.sql +versionable_jamtracks.sql +session_controller.sql +jam_tracks_bpm.sql +jam_track_sessions.sql +jam_track_sessions_v2.sql +email_screening.sql +bounced_email_cleanup.sql +news.sql +profile_teacher.sql +populate_languages.sql +populate_subjects.sql +reviews.sql +download_tracker_fingerprints.sql +connection_active.sql +chat_channel.sql +jamblaster.sql +test_drive_lessons.sql +whitelist.sql +teacher_student_flags.sql +add_sale_source_col.sql +jamblaster_v2.sql +acapella_rename.sql +jamblaster_pairing_active.sql +email_blacklist.sql +jamblaster_connection.sql +teacher_progression.sql +teacher_complete.sql +lessons.sql +lessons_unread_messages.sql +track_school_signups.sql +add_test_drive_types.sql +updated_subjects.sql +update_payment_history.sql +lesson_booking_schools.sql +lesson_booking_schools_2.sql +phantom_accounts.sql +lesson_booking_success.sql +user_origin.sql +remove_stripe_acct_id.sql +track_user_on_lesson.sql +audio_in_music_notations.sql +lesson_time_tracking.sql +packaged_test_drive.sql +packaged_test_drive2.sql +jamclass_report.sql +jamblasters_network.sql +rails4_migration.sql diff --git a/db/up/aac_master.sql b/db/up/aac_master.sql new file mode 100644 index 000000000..28f67f81d --- /dev/null +++ b/db/up/aac_master.sql @@ -0,0 +1,3 @@ +ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_url VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_md5 VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_length bigint; \ No newline at end of file diff --git a/db/up/acapella_rename.sql b/db/up/acapella_rename.sql new file mode 100644 index 000000000..df474501f --- /dev/null +++ b/db/up/acapella_rename.sql @@ -0,0 +1,2 @@ +UPDATE genres set description = 'A Cappella' where id = 'acapella'; + diff --git a/db/up/acappella.sql b/db/up/acappella.sql new file mode 100644 index 000000000..cf17e052d --- /dev/null +++ b/db/up/acappella.sql @@ -0,0 +1,2 @@ +INSERT INTO genres (id, description) values ('acapella', 'A Capella'); +ALTER TABLE jam_track_licensors ADD COLUMN slug VARCHAR UNIQUE; diff --git a/db/up/add_description_to_crash_dumps.sql b/db/up/add_description_to_crash_dumps.sql new file mode 100644 index 000000000..e08540d85 --- /dev/null +++ b/db/up/add_description_to_crash_dumps.sql @@ -0,0 +1 @@ +ALTER TABLE crash_dumps ADD COLUMN description VARCHAR(20000); \ No newline at end of file diff --git a/db/up/add_sale_source_col.sql b/db/up/add_sale_source_col.sql new file mode 100644 index 000000000..5b1607e97 --- /dev/null +++ b/db/up/add_sale_source_col.sql @@ -0,0 +1 @@ +ALTER TABLE sales ADD COLUMN source VARCHAR NOT NULL DEFAULT 'recurly'; diff --git a/db/up/add_test_drive_types.sql b/db/up/add_test_drive_types.sql new file mode 100644 index 000000000..01584db3d --- /dev/null +++ b/db/up/add_test_drive_types.sql @@ -0,0 +1,4 @@ +INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('test-drive-2', 'Test Drive (2)', 'Two reduced-price lessons which you can use to find that ideal teacher.', 'test-drive-2', 29.99); +INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('test-drive-1', 'Test Drive (1)', 'One reduced-price lessons which you can use to find that ideal teacher.', 'test-drive-1', 15.99); +UPDATE lesson_package_types set name = 'Test Drive (4)', package_type = 'test-drive-4' WHERE id = 'test-drive'; +ALTER TABLE users ADD COLUMN lesson_package_type_id VARCHAR(64) REFERENCES lesson_package_types(id); \ No newline at end of file diff --git a/db/up/admin_users.sql b/db/up/admin_users.sql index 8933ceaeb..93a12d91a 100644 --- a/db/up/admin_users.sql +++ b/db/up/admin_users.sql @@ -15,7 +15,6 @@ CREATE TABLE admin_users ( updated_at timestamp without time zone NOT NULL ); -ALTER TABLE public.admin_users OWNER TO postgres; CREATE SEQUENCE admin_users_id_seq START WITH 1 INCREMENT BY 1 @@ -23,7 +22,6 @@ CREATE SEQUENCE admin_users_id_seq NO MAXVALUE CACHE 1; -ALTER TABLE public.admin_users_id_seq OWNER TO postgres; ALTER SEQUENCE admin_users_id_seq OWNED BY admin_users.id; SELECT pg_catalog.setval('admin_users_id_seq', 2, true); ALTER TABLE ONLY admin_users ALTER COLUMN id SET DEFAULT nextval('admin_users_id_seq'::regclass); @@ -45,7 +43,6 @@ CREATE TABLE active_admin_comments ( updated_at timestamp without time zone NOT NULL, namespace character varying(255) ); -ALTER TABLE public.active_admin_comments OWNER TO postgres; CREATE SEQUENCE active_admin_comments_id_seq START WITH 1 INCREMENT BY 1 @@ -53,7 +50,6 @@ CREATE SEQUENCE active_admin_comments_id_seq NO MAXVALUE CACHE 1; -ALTER TABLE public.active_admin_comments_id_seq OWNER TO postgres; ALTER SEQUENCE active_admin_comments_id_seq OWNED BY active_admin_comments.id; SELECT pg_catalog.setval('active_admin_comments_id_seq', 1, false); ALTER TABLE ONLY active_admin_comments ALTER COLUMN id SET DEFAULT nextval('active_admin_comments_id_seq'::regclass); diff --git a/db/up/affiliate_partner_rate.sql b/db/up/affiliate_partner_rate.sql new file mode 100644 index 000000000..d36c80c81 --- /dev/null +++ b/db/up/affiliate_partner_rate.sql @@ -0,0 +1 @@ +ALTER TABLE affiliate_partners ADD COLUMN rate NUMERIC(8,2) DEFAULT 0.10; \ No newline at end of file diff --git a/db/up/audio_in_music_notations.sql b/db/up/audio_in_music_notations.sql new file mode 100644 index 000000000..5774b3c44 --- /dev/null +++ b/db/up/audio_in_music_notations.sql @@ -0,0 +1,5 @@ +ALTER TABLE music_notations ADD COLUMN attachment_type VARCHAR NOT NULL DEFAULT 'notation'; +ALTER TABLE chat_messages ADD PRIMARY KEY (id); +ALTER TABLE music_notations ADD PRIMARY KEY (id); +ALTER TABLE chat_messages ADD COLUMN music_notation_id VARCHAR(64) REFERENCES music_notations(id); +ALTER TABLE chat_messages ADD COLUMN claimed_recording_id VARCHAR(64) REFERENCES claimed_recordings(id); \ No newline at end of file diff --git a/db/up/bounced_email_cleanup.sql b/db/up/bounced_email_cleanup.sql new file mode 100644 index 000000000..0f18b688a --- /dev/null +++ b/db/up/bounced_email_cleanup.sql @@ -0,0 +1,3 @@ +ALTER TABLE generic_state ADD COLUMN bounce_check_at DATE; +UPDATE generic_state SET bounce_check_at = NOW(); +ALTER TABLE users ADD COLUMN bounced BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/db/up/chat_channel.sql b/db/up/chat_channel.sql new file mode 100644 index 000000000..ea0cbba75 --- /dev/null +++ b/db/up/chat_channel.sql @@ -0,0 +1,4 @@ +ALTER TABLE chat_messages ADD COLUMN channel VARCHAR(128) NOT NULL DEFAULT 'session'; +CREATE INDEX chat_messages_idx_channels ON chat_messages(channel); +CREATE INDEX chat_messages_idx_created_at ON chat_messages(created_at); +CREATE INDEX chat_messages_idx_music_session_id ON chat_messages(music_session_id); \ No newline at end of file diff --git a/db/up/connection_active.sql b/db/up/connection_active.sql new file mode 100644 index 000000000..085021aaf --- /dev/null +++ b/db/up/connection_active.sql @@ -0,0 +1 @@ +ALTER TABLE connections ADD COLUMN user_active BOOLEAN DEFAULT TRUE; \ No newline at end of file diff --git a/db/up/crash_dumps_2.sql b/db/up/crash_dumps_2.sql new file mode 100644 index 000000000..96a5374b1 --- /dev/null +++ b/db/up/crash_dumps_2.sql @@ -0,0 +1,6 @@ +ALTER TABLE crash_dumps ADD COLUMN email VARCHAR(255); +ALTER TABLE crash_dumps ADD COLUMN description VARCHAR(10000); +ALTER TABLE crash_dumps ADD COLUMN os VARCHAR(100); +ALTER TABLE crash_dumps ADD COLUMN os_version VARCHAR(100); +ALTER TABLE crash_dumps DROP CONSTRAINT crash_dumps_user_id_fkey; + diff --git a/db/up/download_tracker_fingerprints.sql b/db/up/download_tracker_fingerprints.sql new file mode 100644 index 000000000..1163cfff2 --- /dev/null +++ b/db/up/download_tracker_fingerprints.sql @@ -0,0 +1,3 @@ +ALTER TABLE download_trackers ADD COLUMN fingerprint VARCHAR(1000); +CREATE INDEX index_download_trackers_on_fingerprint ON download_trackers USING btree (fingerprint); +ALTER TABLE download_trackers ADD COLUMN is_client BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/db/up/email_blacklist.sql b/db/up/email_blacklist.sql new file mode 100644 index 000000000..d1b7c30f0 --- /dev/null +++ b/db/up/email_blacklist.sql @@ -0,0 +1,10 @@ +CREATE TABLE email_blacklists ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + email VARCHAR(1000) UNIQUE NOT NULL, + source VARCHAR(1000), + notes VARCHAR(1000), + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL +); + +ALTER TABLE jamblasters DROP COLUMN vtoken; \ No newline at end of file diff --git a/db/up/email_screening.sql b/db/up/email_screening.sql new file mode 100644 index 000000000..7f64c0123 --- /dev/null +++ b/db/up/email_screening.sql @@ -0,0 +1,2 @@ +ALTER TABLE users ADD COLUMN email_needs_verification BOOLEAN DEFAULT FALSE; +ALTER TABLE users ADD COLUMN kickbox_response JSON; \ No newline at end of file diff --git a/db/up/giftcard.sql b/db/up/giftcard.sql new file mode 100644 index 000000000..eae33c13e --- /dev/null +++ b/db/up/giftcard.sql @@ -0,0 +1,13 @@ +CREATE TABLE gift_cards ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + code VARCHAR(64) UNIQUE NOT NULL, + user_id VARCHAR (64) REFERENCES users(id) ON DELETE CASCADE, + card_type VARCHAR(64) NOT NULL, + origin VARCHAR(200), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX gift_card_user_id_idx ON gift_cards(user_id); + +ALTER TABLE users ADD COLUMN gifted_jamtracks INTEGER DEFAULT 0; diff --git a/db/up/jam_track_lang_idx.sql b/db/up/jam_track_lang_idx.sql new file mode 100644 index 000000000..aa5c84c26 --- /dev/null +++ b/db/up/jam_track_lang_idx.sql @@ -0,0 +1 @@ +CREATE INDEX ON jam_tracks(language); diff --git a/db/up/jam_track_sessions.sql b/db/up/jam_track_sessions.sql new file mode 100644 index 000000000..7936b5031 --- /dev/null +++ b/db/up/jam_track_sessions.sql @@ -0,0 +1,9 @@ +CREATE TABLE jam_track_sessions ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + jam_track_id VARCHAR(64) NOT NULL REFERENCES jam_tracks(id) ON DELETE CASCADE, + session_type VARCHAR(10) NOT NULL, + music_session_id VARCHAR(64) REFERENCES music_sessions(id) ON DELETE SET NULL, + user_id VARCHAR(64) NOT NULL REFERENCES users(id), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/db/up/jam_track_sessions_v2.sql b/db/up/jam_track_sessions_v2.sql new file mode 100644 index 000000000..cf1c6dab6 --- /dev/null +++ b/db/up/jam_track_sessions_v2.sql @@ -0,0 +1,2 @@ +ALTER TABLE jam_track_sessions ALTER COLUMN user_id DROP NOT NULL; +ALTER TABLE crash_dumps ALTER COLUMN user_id DROP NOT NULL; \ No newline at end of file diff --git a/db/up/jam_tracks_bpm.sql b/db/up/jam_tracks_bpm.sql new file mode 100644 index 000000000..0fce5154d --- /dev/null +++ b/db/up/jam_tracks_bpm.sql @@ -0,0 +1,2 @@ +ALTER TABLE jam_tracks ADD COLUMN bpm numeric(8,3); +INSERT INTO instruments (id, description) VALUES ('percussion', 'Percussion'); \ No newline at end of file diff --git a/db/up/jamblaster.sql b/db/up/jamblaster.sql new file mode 100644 index 000000000..56760576a --- /dev/null +++ b/db/up/jamblaster.sql @@ -0,0 +1,28 @@ +CREATE TABLE jamblasters ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE SET NULL, + serial_no VARCHAR(1000) UNIQUE, + vtoken VARCHAR(1000) UNIQUE, + client_id VARCHAR(64) UNIQUE, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL +); + +CREATE TABLE jamblasters_users ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + jamblaster_id VARCHAR(64) NOT NULL REFERENCES jamblasters(id) ON DELETE CASCADE, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL +); + +CREATE TABLE jamblaster_pairing_requests ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + jamblaster_id VARCHAR(64) NOT NULL REFERENCES jamblasters(id) ON DELETE CASCADE, + jamblaster_client_id VARCHAR(64) NOT NULL, + sibling_client_id VARCHAR(64) NOT NULL, + sibling_key VARCHAR(1000) NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL +); diff --git a/db/up/jamblaster_connection.sql b/db/up/jamblaster_connection.sql new file mode 100644 index 000000000..3597af2fd --- /dev/null +++ b/db/up/jamblaster_connection.sql @@ -0,0 +1 @@ +ALTER TABLE connections ADD COLUMN is_jamblaster BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/db/up/jamblaster_pairing_active.sql b/db/up/jamblaster_pairing_active.sql new file mode 100644 index 000000000..057fd895d --- /dev/null +++ b/db/up/jamblaster_pairing_active.sql @@ -0,0 +1 @@ +ALTER TABLE jamblaster_pairing_requests ADD COLUMN active BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/db/up/jamblaster_v2.sql b/db/up/jamblaster_v2.sql new file mode 100644 index 000000000..6f29d461a --- /dev/null +++ b/db/up/jamblaster_v2.sql @@ -0,0 +1,3 @@ +ALTER TABLE jamblaster_pairing_requests ALTER COLUMN sibling_key DROP NOT NULL; +ALTER TABLE jamblaster_pairing_requests ADD COLUMN vtoken VARCHAR(400) NOT NULL; +ALTER TABLE jamblaster_pairing_requests DROP COLUMN sibling_client_id; \ No newline at end of file diff --git a/db/up/jamblasters_network.sql b/db/up/jamblasters_network.sql new file mode 100644 index 000000000..e986360cd --- /dev/null +++ b/db/up/jamblasters_network.sql @@ -0,0 +1,3 @@ +ALTER TABLE jamblasters ADD COLUMN ipv6_link_local VARCHAR; +ALTER TABLE jamblasters ADD COLUMN ipv4_link_local VARCHAR; +ALTER TABLE jamblasters ADD COLUMN display_name VARCHAR; \ No newline at end of file diff --git a/db/up/jamclass_report.sql b/db/up/jamclass_report.sql new file mode 100644 index 000000000..427b9ab0b --- /dev/null +++ b/db/up/jamclass_report.sql @@ -0,0 +1,33 @@ +CREATE TABLE campaign_spends ( + id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + campaign VARCHAR NOT NULL, + spend NUMERIC(8,2) NOT NULL, + month INTEGER NOT NULL, + year INTEGER NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE jam_class_reports ( + cohort DATE, + campaign VARCHAR, + spend NUMERIC (8,2), + registrations INTEGER, + td_customers INTEGER, + jamclass_rev NUMERIC (8,2), + td4 INTEGER, + td2 INTEGER, + td1 INTEGER, + spend_td NUMERIC (8,2), + purchases0 NUMERIC (8,2), + purchases1 NUMERIC (8,2), + purchases2 NUMERIC (8,2), + purchases3 NUMERIC (8,2), + purchases_rest NUMERIC (8,2), + purchases0_count INTEGER, + purchases1_count INTEGER, + purchases2_count INTEGER, + purchases3_count INTEGER, + purchases_rest_count INTEGER, + purchases_count INTEGER +); \ No newline at end of file diff --git a/db/up/lesson_booking_schools.sql b/db/up/lesson_booking_schools.sql new file mode 100644 index 000000000..265e731b7 --- /dev/null +++ b/db/up/lesson_booking_schools.sql @@ -0,0 +1,3 @@ +ALTER TABLE lesson_bookings ADD COLUMN school_id INTEGER REFERENCES schools(id); +ALTER TABLE teacher_payments ADD COLUMN school_id INTEGER REFERENCES schools(id); +ALTER TABLE teacher_distributions ADD COLUMN school_id INTEGER REFERENCES schools(id); \ No newline at end of file diff --git a/db/up/lesson_booking_schools_2.sql b/db/up/lesson_booking_schools_2.sql new file mode 100644 index 000000000..7888cbeb8 --- /dev/null +++ b/db/up/lesson_booking_schools_2.sql @@ -0,0 +1,2 @@ +ALTER TABLE lesson_bookings ADD COLUMN same_school BOOLEAN DEFAULT FALSE NOT NULL; +ALTER TABLE schools ADD COLUMN affiliate_partner_id INTEGER REFERENCES affiliate_partners(id); \ No newline at end of file diff --git a/db/up/lesson_booking_success.sql b/db/up/lesson_booking_success.sql new file mode 100644 index 000000000..3dae1ae2f --- /dev/null +++ b/db/up/lesson_booking_success.sql @@ -0,0 +1 @@ +ALTER TABLE lesson_bookings ADD COLUMN success BOOLEAN; \ No newline at end of file diff --git a/db/up/lesson_time_tracking.sql b/db/up/lesson_time_tracking.sql new file mode 100644 index 000000000..9141b0bd8 --- /dev/null +++ b/db/up/lesson_time_tracking.sql @@ -0,0 +1,6 @@ +ALTER TABLE lesson_bookings ADD COLUMN sent_notices_at timestamp without time zone; +ALTER TABLE lesson_bookings ADD COLUMN countered_at timestamp without time zone; +ALTER TABLE lesson_sessions ADD COLUMN countered_at timestamp without time zone; +ALTER TABLE lesson_bookings ADD COLUMN counterer_id VARCHAR(64) REFERENCES users(id); +ALTER TABLE lesson_sessions ADD COLUMN counterer_id VARCHAR(64) REFERENCES users(id); +ALTER TABLE lesson_bookings ADD COLUMN sent_counter_reminder BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/db/up/lessons.sql b/db/up/lessons.sql new file mode 100644 index 000000000..bc6ff6daf --- /dev/null +++ b/db/up/lessons.sql @@ -0,0 +1,261 @@ + +CREATE TABLE lesson_package_types ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR NOT NULL, + description VARCHAR NOT NULL, + package_type VARCHAR(64) NOT NULL, + price NUMERIC(8,2), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE lesson_bookings ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) REFERENCES users(id) NOT NULL, + active BOOLEAN NOT NULL DEFAULT FALSE, + accepter_id VARCHAR(64) REFERENCES users(id), + canceler_id VARCHAR(64) REFERENCES users(id), + lesson_type VARCHAR(64) NOT NULL, + recurring BOOLEAN NOT NULL, + lesson_length INTEGER NOT NULL, + payment_style VARCHAR(64) NOT NULL, + description VARCHAR, + booked_price NUMERIC(8,2) NOT NULL, + teacher_id VARCHAR(64) REFERENCES users(id) NOT NULL, + card_presumed_ok BOOLEAN NOT NULL DEFAULT FALSE, + sent_notices BOOLEAN NOT NULL DEFAULT FALSE, + status VARCHAR, + cancel_message VARCHAR, + user_decremented BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE charges ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + amount_in_cents INTEGER NOT NULL, + fee_in_cents INTEGER NOT NULL DEFAULT 0, + type VARCHAR(64) NOT NULL, + sent_billing_notices BOOLEAN NOT NULL DEFAULT FALSE, + sent_billing_notices_at TIMESTAMP, + last_billing_attempt_at TIMESTAMP, + billed BOOLEAN NOT NULL DEFAULT FALSE, + billed_at TIMESTAMP, + post_processed BOOLEAN NOT NULL DEFAULT FALSE, + post_processed_at TIMESTAMP, + billing_error_reason VARCHAR, + billing_error_detail VARCHAR, + billing_should_retry BOOLEAN NOT NULL DEFAULT TRUE , + billing_attempts INTEGER NOT NULL DEFAULT 0, + stripe_charge_id VARCHAR(200), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE lesson_package_purchases ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + lesson_package_type_id VARCHAR(64) REFERENCES lesson_package_types(id) NOT NULL, + user_id VARCHAR(64) REFERENCES users(id) NOT NULL, + teacher_id VARCHAR(64) REFERENCES users(id), + price NUMERIC(8,2), + recurring BOOLEAN NOT NULL DEFAULT FALSE, + year INTEGER, + month INTEGER, + charge_id VARCHAR(64) REFERENCES charges(id), + lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id), + sent_notices BOOLEAN NOT NULL DEFAULT FALSE, + sent_notices_at TIMESTAMP, + post_processed BOOLEAN NOT NULL DEFAULT FALSE, + post_processed_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +CREATE TABLE lesson_sessions ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + lesson_type VARCHAR(64) NOT NULL, + teacher_id VARCHAR(64) REFERENCES users(id) NOT NULL, + lesson_package_purchase_id VARCHAR(64) REFERENCES lesson_package_purchases(id), + lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id), + duration INTEGER NOT NULL, + booked_price NUMERIC(8,2) NOT NULL, + teacher_complete BOOLEAN DEFAULT FALSE NOT NULL, + student_complete BOOLEAN DEFAULT FALSE NOT NULL, + student_canceled BOOLEAN DEFAULT FALSE NOT NULL, + teacher_canceled BOOLEAN DEFAULT FALSE NOT NULL, + student_canceled_at TIMESTAMP, + teacher_canceled_at TIMESTAMP, + student_canceled_reason VARCHAR, + teacher_canceled_reason VARCHAR, + status VARCHAR, + analysed BOOLEAN NOT NULL DEFAULT FALSE, + analysis JSON, + analysed_at TIMESTAMP, + cancel_message VARCHAR, + canceler_id VARCHAR(64) REFERENCES users(id), + charge_id VARCHAR(64) REFERENCES charges(id), + success BOOLEAN NOT NULL DEFAULT FALSE, + sent_notices BOOLEAN NOT NULL DEFAULT FALSE, + sent_notices_at TIMESTAMP, + post_processed BOOLEAN NOT NULL DEFAULT FALSE, + post_processed_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE music_sessions ADD COLUMN lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id); +ALTER TABLE notifications ADD COLUMN lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id); +ALTER TABLE notifications ADD COLUMN purpose VARCHAR(200); +ALTER TABLE notifications ADD COLUMN student_directed BOOLEAN; + +INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('single', 'Single Lesson', 'A single lesson purchased at the teacher''s price.', 'single', 0.00); +INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('single-free', 'Free Lesson', 'A free, single lesson.', 'single-free', 0.00); +INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('test-drive', 'Test Drive', 'Four reduced-price lessons which you can use to find that ideal teacher.', 'test-drive', 49.99); + + +CREATE TABLE lesson_booking_slots ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id), + lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id), + slot_type VARCHAR(64) NOT NULL, + preferred_day DATE, + day_of_week INTEGER, + hour INTEGER, + minute INTEGER, + timezone VARCHAR NOT NULL, + message VARCHAR, + accept_message VARCHAR, + update_all BOOLEAN NOT NULL DEFAULT FALSE, + proposer_id VARCHAR(64) REFERENCES users(id) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE lesson_bookings ADD COLUMN default_slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id); +ALTER TABLE lesson_bookings ADD COLUMN counter_slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id); +ALTER TABLE lesson_sessions ADD COLUMN counter_slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id); +ALTER TABLE lesson_sessions ADD COLUMN slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id); + +ALTER TABLE chat_messages ADD COLUMN target_user_id VARCHAR(64) REFERENCES users(id); +ALTER TABLE chat_messages ADD COLUMN lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id); +ALTER TABLE users ADD COLUMN remaining_free_lessons INTEGER NOT NULL DEFAULT 1; +ALTER TABLE users ADD COLUMN stored_credit_card BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE users ADD COLUMN remaining_test_drives INTEGER NOT NULL DEFAULT 0; +ALTER TABLE users ADD COLUMN stripe_token VARCHAR(200); +ALTER TABLE users ADD COLUMN stripe_customer_id VARCHAR(200); +ALTER TABLE users ADD COLUMN stripe_zip_code VARCHAR(200); +ALTER TABLE sales ADD COLUMN stripe_charge_id VARCHAR(200); +ALTER TABLE teachers ADD COLUMN stripe_account_id VARCHAR(200); +ALTER TABLE sale_line_items ADD COLUMN lesson_package_purchase_id VARCHAR(64) REFERENCES lesson_package_purchases(id); + + +-- one is created every time the teacher is paid. N teacher_distributions point to this +CREATE TABLE teacher_payments ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + teacher_id VARCHAR(64) REFERENCES users(id) NOT NULL, + charge_id VARCHAR(64) REFERENCES charges(id) NOT NULL, + amount_in_cents INTEGER NOT NULL, + fee_in_cents INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- one is created for every bit of money the teacher is due +CREATE TABLE teacher_distributions ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + teacher_id VARCHAR(64) REFERENCES users(id) NOT NULL, + teacher_payment_id VARCHAR(64) REFERENCES teacher_payments(id), + lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id), + lesson_package_purchase_id VARCHAR(64) REFERENCES lesson_package_purchases(id), + amount_in_cents INTEGER NOT NULL, + ready BOOLEAN NOT NULL DEFAULT FALSE, + distributed BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE affiliate_distributions ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + affiliate_referral_id INTEGER REFERENCES affiliate_partners(id) NOT NULL, + affiliate_referral_fee_in_cents INTEGER NOT NULL, + sale_line_item_id VARCHAR(64) REFERENCES sale_line_items(id) NOT NULL, + affiliate_refunded BOOLEAN NOT NULL DEFAULT FALSE, + affiliate_refunded_at TIMESTAMP WITHOUT TIME ZONE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE affiliate_partners ADD COLUMN lesson_rate NUMERIC (8,2) NOT NULL DEFAULT 0.20; + +-- move over all sale_line_item affiliate info +INSERT INTO affiliate_distributions ( + SELECT + sale_line_items.id, + sale_line_items.affiliate_referral_id, + sale_line_items.affiliate_referral_fee_in_cents, + sale_line_items.id, + sale_line_items.affiliate_refunded, + sale_line_items.affiliate_refunded_at, + sale_line_items.created_at, + sale_line_items.updated_at + FROM sale_line_items + WHERE sale_line_items.affiliate_referral_id IS NOT NULL +); + +CREATE TABLE teacher_intents ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) REFERENCES users(id) NOT NULL, + teacher_id VARCHAR(64) REFERENCES teachers(id) NOT NULL, + intent VARCHAR(64), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX teacher_intents_intent_idx ON teacher_intents(teacher_id, intent); + +CREATE TABLE schools ( + id INTEGER PRIMARY KEY, + user_id VARCHAR(64) REFERENCES users(id) NOT NULL, + name VARCHAR, + enabled BOOLEAN DEFAULT TRUE, + scheduling_communication VARCHAR NOT NULL DEFAULT 'teacher', + correspondence_email VARCHAR, + photo_url VARCHAR(2048), + original_fpfile VARCHAR(8000), + cropped_fpfile VARCHAR(8000), + cropped_s3_path VARCHAR(8000), + crop_selection VARCHAR(256), + large_photo_url VARCHAR(512), + cropped_large_s3_path VARCHAR(512), + cropped_large_fpfile VARCHAR(8000), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE SEQUENCE school_key_sequence; +ALTER SEQUENCE school_key_sequence RESTART WITH 10000; +ALTER TABLE schools ALTER COLUMN id SET DEFAULT nextval('school_key_sequence'); + +ALTER TABLE users ADD COLUMN school_id INTEGER REFERENCES schools(id); +ALTER TABLE users ADD COLUMN joined_school_at TIMESTAMP; +ALTER TABLE teachers ADD COLUMN school_id INTEGER REFERENCES schools(id); +ALTER TABLE teachers ADD COLUMN joined_school_at TIMESTAMP; + +CREATE TABLE school_invitations ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) REFERENCES users(id), + school_id INTEGER REFERENCES schools(id) NOT NULL, + invitation_code VARCHAR(256) NOT NULL UNIQUE, + note VARCHAR, + as_teacher BOOLEAN NOT NULL, + email VARCHAR NOT NULL, + first_name VARCHAR, + last_name VARCHAR, + accepted BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE teachers ADD jamkazam_rate NUMERIC (8, 2) DEFAULT 0.25; +ALTER TABLE schools ADD jamkazam_rate NUMERIC (8, 2) DEFAULT 0.25; \ No newline at end of file diff --git a/db/up/lessons_unread_messages.sql b/db/up/lessons_unread_messages.sql new file mode 100644 index 000000000..1b2d65f26 --- /dev/null +++ b/db/up/lessons_unread_messages.sql @@ -0,0 +1,34 @@ +ALTER TABLE chat_messages DROP COLUMN lesson_booking_id; +ALTER TABLE chat_messages ADD COLUMN lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id); +ALTER TABLE lesson_sessions ADD COLUMN teacher_unread_messages BOOLEAN DEFAULT FALSE NOT NULL; +ALTER TABLE lesson_sessions ADD COLUMN student_unread_messages BOOLEAN DEFAULT FALSE NOT NULL; +ALTER TABLE chat_messages ADD COLUMN purpose VARCHAR(200); +ALTER TABLE lesson_sessions ADD COLUMN student_short_canceled BOOLEAN DEFAULT FALSE NOT NULL; +ALTER TABLE lesson_sessions ADD COLUMN teacher_short_canceled BOOLEAN DEFAULT FALSE NOT NULL; +ALTER TABLE lesson_sessions ADD COLUMN sent_starting_notice BOOLEAN DEFAULT FALSE NOT NULL; + +ALTER TABLE lesson_bookings DROP CONSTRAINT lesson_bookings_counter_slot_id_fkey; +ALTER TABLE lesson_bookings ADD CONSTRAINT lesson_bookings_counter_slot_id_fkey FOREIGN KEY (counter_slot_id) REFERENCES lesson_booking_slots(id) ON DELETE CASCADE; + +ALTER TABLE lesson_bookings DROP CONSTRAINT lesson_bookings_default_slot_id_fkey; +ALTER TABLE lesson_bookings ADD CONSTRAINT lesson_bookings_default_slot_id_fkey FOREIGN KEY (default_slot_id) REFERENCES lesson_booking_slots(id) ON DELETE CASCADE; + + +ALTER TABLE lesson_sessions DROP CONSTRAINT lesson_sessions_slot_id_fkey; +ALTER TABLE lesson_sessions ADD CONSTRAINT lesson_sessions_slot_id_fkey FOREIGN KEY (slot_id) REFERENCES lesson_booking_slots(id) ON DELETE CASCADE; + +ALTER TABLE users DROP CONSTRAINT users_teacher_id_fkey; +ALTER TABLE users ADD CONSTRAINT users_teacher_id_fkey FOREIGN KEY (teacher_id) REFERENCES teachers(id) ON DELETE CASCADE; + +ALTER TABLE music_sessions DROP CONSTRAINT music_sessions_lesson_session_id_fkey; +ALTER TABLE music_sessions ADD CONSTRAINT music_sessions_lesson_session_id_fkey FOREIGN KEY (lesson_session_id) REFERENCES lesson_sessions(id) ON DELETE SET NULL; + +ALTER TABLE notifications DROP CONSTRAINT notifications_lesson_session_id_fkey; +ALTER TABLE notifications ADD CONSTRAINT notifications_lesson_session_id_fkey FOREIGN KEY (lesson_session_id) REFERENCES lesson_sessions(id) ON DELETE CASCADE; + +ALTER TABLE chat_messages DROP CONSTRAINT chat_messages_lesson_session_id_fkey; +ALTER TABLE chat_messages ADD CONSTRAINT chat_messages_lesson_session_id_fkey FOREIGN KEY (lesson_session_id) REFERENCES lesson_sessions(id) ON DELETE CASCADE; + +ALTER TABLE chat_messages DROP CONSTRAINT chat_messages_target_user_id_fkey; +ALTER TABLE chat_messages ADD CONSTRAINT chat_messages_target_user_id_fkey FOREIGN KEY (lesson_session_id) REFERENCES lesson_sessions(id) ON DELETE SET NULL; + diff --git a/db/up/mixdown.sql b/db/up/mixdown.sql new file mode 100644 index 000000000..24295cdae --- /dev/null +++ b/db/up/mixdown.sql @@ -0,0 +1,61 @@ +CREATE TABLE jam_track_mixdowns ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + jam_track_id VARCHAR(64) NOT NULL REFERENCES jam_tracks(id) ON DELETE CASCADE, + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + settings JSON NOT NULL, + name VARCHAR(1000) NOT NULL, + description VARCHAR(1000), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE jam_track_mixdown_packages ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + jam_track_mixdown_id VARCHAR(64) NOT NULL REFERENCES jam_track_mixdowns(id) ON DELETE CASCADE, + file_type VARCHAR NOT NULL , + sample_rate INTEGER NOT NULL, + url VARCHAR(2048), + md5 VARCHAR, + length INTEGER, + downloaded_since_sign BOOLEAN NOT NULL DEFAULT FALSE, + last_step_at TIMESTAMP, + last_signed_at TIMESTAMP, + download_count INTEGER NOT NULL DEFAULT 0, + signed_at TIMESTAMP, + downloaded_at TIMESTAMP, + signing_queued_at TIMESTAMP, + error_count INTEGER NOT NULL DEFAULT 0, + error_reason VARCHAR, + error_detail VARCHAR, + should_retry BOOLEAN NOT NULL DEFAULT FALSE, + packaging_steps INTEGER, + current_packaging_step INTEGER, + private_key VARCHAR, + signed BOOLEAN, + signing_started_at TIMESTAMP, + first_downloaded TIMESTAMP, + signing BOOLEAN NOT NULL DEFAULT FALSE, + encrypt_type VARCHAR, + first_downloaded_at TIMESTAMP, + last_downloaded_at TIMESTAMP, + version VARCHAR NOT NULL DEFAULT '1', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE jam_track_rights ADD COLUMN last_mixdown_id VARCHAR(64) REFERENCES jam_track_mixdowns(id) ON DELETE SET NULL; + +ALTER TABLE notifications ADD COLUMN jam_track_mixdown_package_id VARCHAR(64) REFERENCES jam_track_mixdown_packages(id) ON DELETE CASCADE; + +ALTER TABLE jam_track_mixdown_packages ADD COLUMN last_errored_at TIMESTAMP; +ALTER TABLE jam_track_mixdown_packages ADD COLUMN queued BOOLEAN DEFAULT FALSE; +ALTER TABLE jam_track_mixdown_packages ADD COLUMN speed_pitched BOOLEAN DEFAULT FALSE; +ALTER TABLE jam_track_rights ADD COLUMN queued BOOLEAN DEFAULT FALSE; + +CREATE INDEX jam_track_rights_queued ON jam_track_rights(queued); +CREATE INDEX jam_track_rights_signing_queued ON jam_track_rights(signing_queued_at); +CREATE INDEX jam_track_rights_updated ON jam_track_rights(updated_at); + +CREATE INDEX jam_track_mixdown_packages_queued ON jam_track_mixdown_packages(queued); +CREATE INDEX jam_track_mixdown_packages_signing_queued ON jam_track_mixdown_packages(signing_queued_at); +CREATE INDEX jam_track_mixdown_packages_updated ON jam_track_mixdown_packages(updated_at); diff --git a/db/up/news.sql b/db/up/news.sql new file mode 100644 index 000000000..2eb981de8 --- /dev/null +++ b/db/up/news.sql @@ -0,0 +1,8 @@ +CREATE TABLE news ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + title VARCHAR NOT NULL, + body VARCHAR NOT NULL, + position INTEGER NOT NULL UNIQUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/db/up/packaged_test_drive.sql b/db/up/packaged_test_drive.sql new file mode 100644 index 000000000..fb1796448 --- /dev/null +++ b/db/up/packaged_test_drive.sql @@ -0,0 +1,38 @@ +CREATE TABLE test_drive_packages ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR UNIQUE NOT NULL, + package_type VARCHAR NOT NULL, + description VARCHAR, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE test_drive_package_teachers ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE, + test_drive_package_id VARCHAR(64) REFERENCES test_drive_packages(id) ON DELETE CASCADE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE lesson_bookings ADD COLUMN test_drive_package_id VARCHAR(64) REFERENCES test_drive_packages(id); + +CREATE TABLE test_drive_package_choices ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + test_drive_package_id VARCHAR(64) REFERENCES test_drive_packages(id) ON DELETE CASCADE, + user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +CREATE TABLE test_drive_package_choice_teachers ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + test_drive_package_choice_id VARCHAR(64) REFERENCES test_drive_package_choices(id) ON DELETE CASCADE, + teacher_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE teachers ADD COLUMN short_bio VARCHAR; +ALTER TABLE test_drive_package_teachers ADD COLUMN short_bio VARCHAR; \ No newline at end of file diff --git a/db/up/packaged_test_drive2.sql b/db/up/packaged_test_drive2.sql new file mode 100644 index 000000000..c5c7209c9 --- /dev/null +++ b/db/up/packaged_test_drive2.sql @@ -0,0 +1,2 @@ +ALTER TABLE lesson_booking_slots ADD COLUMN from_package BOOL DEFAULT FALSE; +ALTER TABLE lesson_bookings ADD COLUMN test_drive_package_choice_id VARCHAR(64) REFERENCES test_drive_package_choices(id); diff --git a/db/up/phantom_accounts.sql b/db/up/phantom_accounts.sql new file mode 100644 index 000000000..696c57370 --- /dev/null +++ b/db/up/phantom_accounts.sql @@ -0,0 +1,16 @@ +ALTER TABLE users ADD COLUMN phantom BOOLEAN DEFAULT FALSE NOT NULL; + +CREATE OR REPLACE FUNCTION phantom_check() RETURNS TRIGGER +STRICT VOLATILE AS $$ + BEGIN + + -- Remember who changed the payroll when + NEW.phantom := (SELECT NEW.email ilike 'phantom+%@jamkazam.com'); + RETURN NEW; + END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER phantom_update BEFORE INSERT OR UPDATE +ON users FOR EACH ROW EXECUTE PROCEDURE phantom_check(id); + +UPDATE users set updated_at = NOW(); diff --git a/db/up/populate_languages.sql b/db/up/populate_languages.sql new file mode 100644 index 000000000..885995b69 --- /dev/null +++ b/db/up/populate_languages.sql @@ -0,0 +1,72 @@ +insert into languages(description, id) values ('English','EN'); +insert into languages(description, id) values ('Afrikanns','AF'); +insert into languages(description, id) values ('Albanian','SQ'); +insert into languages(description, id) values ('Arabic','AR'); +insert into languages(description, id) values ('Armenian','HY'); +insert into languages(description, id) values ('Basque','EU'); +insert into languages(description, id) values ('Bengali','BN'); +insert into languages(description, id) values ('Bulgarian','BG'); +insert into languages(description, id) values ('Catalan','CA'); +insert into languages(description, id) values ('Cambodian','KM'); +insert into languages(description, id) values ('Chinese (Mandarin)','ZH'); +insert into languages(description, id) values ('Croation','HR'); +insert into languages(description, id) values ('Czech','CS'); +insert into languages(description, id) values ('Danish','DA'); +insert into languages(description, id) values ('Dutch','NL'); +insert into languages(description, id) values ('Estonian','ET'); +insert into languages(description, id) values ('Fiji','FJ'); +insert into languages(description, id) values ('Finnish','FI'); +insert into languages(description, id) values ('French','FR'); +insert into languages(description, id) values ('Georgian','KA'); +insert into languages(description, id) values ('German','DE'); +insert into languages(description, id) values ('Greek','EL'); +insert into languages(description, id) values ('Gujarati','GU'); +insert into languages(description, id) values ('Hebrew','HE'); +insert into languages(description, id) values ('Hindi','HI'); +insert into languages(description, id) values ('Hungarian','HU'); +insert into languages(description, id) values ('Icelandic','IS'); +insert into languages(description, id) values ('Indonesian','ID'); +insert into languages(description, id) values ('Irish','GA'); +insert into languages(description, id) values ('Italian','IT'); +insert into languages(description, id) values ('Japanese','JA'); +insert into languages(description, id) values ('Javanese','JW'); +insert into languages(description, id) values ('Korean','KO'); +insert into languages(description, id) values ('Latin','LA'); +insert into languages(description, id) values ('Latvian','LV'); +insert into languages(description, id) values ('Lithuanian','LT'); +insert into languages(description, id) values ('Macedonian','MK'); +insert into languages(description, id) values ('Malay','MS'); +insert into languages(description, id) values ('Malayalam','ML'); +insert into languages(description, id) values ('Maltese','MT'); +insert into languages(description, id) values ('Maori','MI'); +insert into languages(description, id) values ('Marathi','MR'); +insert into languages(description, id) values ('Mongolian','MN'); +insert into languages(description, id) values ('Nepali','NE'); +insert into languages(description, id) values ('Norwegian','NO'); +insert into languages(description, id) values ('Persian','FA'); +insert into languages(description, id) values ('Polish','PL'); +insert into languages(description, id) values ('Portuguese','PT'); +insert into languages(description, id) values ('Punjabi','PA'); +insert into languages(description, id) values ('Quechua','QU'); +insert into languages(description, id) values ('Romanian','RO'); +insert into languages(description, id) values ('Russian','RU'); +insert into languages(description, id) values ('Samoan','SM'); +insert into languages(description, id) values ('Serbian','SR'); +insert into languages(description, id) values ('Slovak','SK'); +insert into languages(description, id) values ('Slovenian','SL'); +insert into languages(description, id) values ('Spanish','ES'); +insert into languages(description, id) values ('Swahili','SW'); +insert into languages(description, id) values ('Swedish ','SV'); +insert into languages(description, id) values ('Tamil','TA'); +insert into languages(description, id) values ('Tatar','TT'); +insert into languages(description, id) values ('Telugu','TE'); +insert into languages(description, id) values ('Thai','TH'); +insert into languages(description, id) values ('Tibetan','BO'); +insert into languages(description, id) values ('Tonga','TO'); +insert into languages(description, id) values ('Turkish','TR'); +insert into languages(description, id) values ('Ukranian','UK'); +insert into languages(description, id) values ('Urdu','UR'); +insert into languages(description, id) values ('Uzbek','UZ'); +insert into languages(description, id) values ('Vietnamese','VI'); +insert into languages(description, id) values ('Welsh','CY'); +insert into languages(description, id) values ('Xhosa','XH'); diff --git a/db/up/populate_subjects.sql b/db/up/populate_subjects.sql new file mode 100644 index 000000000..fa433387a --- /dev/null +++ b/db/up/populate_subjects.sql @@ -0,0 +1,6 @@ +insert into subjects(id, description) values ('arranging', 'Arranging'); +insert into subjects(id, description) values ('composing', 'Composing'); +insert into subjects(id, description) values ('music-business', 'Music Business'); +insert into subjects(id, description) values ('music-theory', 'Music Theory'); +insert into subjects(id, description) values ('recording', 'Recording'); +insert into subjects(id, description) values ('site-reading', 'Site Reading'); diff --git a/db/up/profile_teacher.sql b/db/up/profile_teacher.sql new file mode 100644 index 000000000..e4d0bd0b1 --- /dev/null +++ b/db/up/profile_teacher.sql @@ -0,0 +1,75 @@ +CREATE TABLE teachers ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + introductory_video VARCHAR(1024) NULL, + years_teaching SMALLINT NOT NULL DEFAULT 0, + years_playing SMALLINT NOT NULL DEFAULT 0, + teaches_age_lower SMALLINT NOT NULL DEFAULT 0, + teaches_age_upper SMALLINT NOT NULL DEFAULT 0, + teaches_beginner BOOLEAN NOT NULL DEFAULT FALSE, + teaches_intermediate BOOLEAN NOT NULL DEFAULT FALSE, + teaches_advanced BOOLEAN NOT NULL DEFAULT FALSE, + website VARCHAR(1024) NULL, + biography VARCHAR(4096) NULL, + prices_per_lesson BOOLEAN NOT NULL DEFAULT FALSE, + prices_per_month BOOLEAN NOT NULL DEFAULT FALSE, + lesson_duration_30 BOOLEAN NOT NULL DEFAULT FALSE, + lesson_duration_45 BOOLEAN NOT NULL DEFAULT FALSE, + lesson_duration_60 BOOLEAN NOT NULL DEFAULT FALSE, + lesson_duration_90 BOOLEAN NOT NULL DEFAULT FALSE, + lesson_duration_120 BOOLEAN NOT NULL DEFAULT FALSE, + price_per_lesson_30_cents INT NULL, + price_per_lesson_45_cents INT NULL, + price_per_lesson_60_cents INT NULL, + price_per_lesson_90_cents INT NULL, + price_per_lesson_120_cents INT NULL, + price_per_month_30_cents INT NULL, + price_per_month_45_cents INT NULL, + price_per_month_60_cents INT NULL, + price_per_month_90_cents INT NULL, + price_per_month_120_cents INT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE users ADD COLUMN teacher_id VARCHAR(64) REFERENCES teachers(id) ON DELETE SET NULL; + +CREATE TABLE subjects( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + description VARCHAR(1024) NULL +); + +CREATE TABLE languages( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + description VARCHAR(1024) NULL +); + +-- Has many: +CREATE TABLE teacher_experiences( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + teacher_id VARCHAR(64) REFERENCES teachers(id) ON DELETE CASCADE, + -- experience type: teaching, education, award: + experience_type VARCHAR(32) NOT NULL, + name VARCHAR(200) NOT NULL, + organization VARCHAR(200) NOT NULL, + start_year SMALLINT NOT NULL DEFAULT 0, + end_year SMALLINT NULL +); + +-- Has many/through tables: +CREATE TABLE teachers_genres( + teacher_id VARCHAR(64) REFERENCES teachers(id) ON DELETE CASCADE, + genre_id VARCHAR(64) REFERENCES genres(id) ON DELETE CASCADE +); +CREATE TABLE teachers_instruments( + teacher_id VARCHAR(64) REFERENCES teachers(id) ON DELETE CASCADE, + instrument_id VARCHAR(64) REFERENCES instruments(id) ON DELETE CASCADE +); +CREATE TABLE teachers_subjects( + teacher_id VARCHAR(64) REFERENCES teachers(id) ON DELETE CASCADE, + subject_id VARCHAR(64) REFERENCES subjects(id) ON DELETE CASCADE +); +CREATE TABLE teachers_languages( + teacher_id VARCHAR(64) REFERENCES teachers(id) ON DELETE CASCADE, + language_id VARCHAR(64) REFERENCES languages(id) ON DELETE CASCADE +); + diff --git a/db/up/purchasable_gift_cards.sql b/db/up/purchasable_gift_cards.sql new file mode 100644 index 000000000..9ef8d29fa --- /dev/null +++ b/db/up/purchasable_gift_cards.sql @@ -0,0 +1,24 @@ + + +CREATE TABLE gift_card_types ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + card_type VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO gift_card_types (id, card_type) VALUES ('jam_tracks_5', 'jam_tracks_5'); +INSERT INTO gift_card_types (id, card_type) VALUES ('jam_tracks_10', 'jam_tracks_10'); + +CREATE TABLE gift_card_purchases ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE SET NULL, + gift_card_type_id VARCHAR(64) REFERENCES gift_card_types(id) ON DELETE SET NULL, + recurly_adjustment_uuid VARCHAR(500), + recurly_adjustment_credit_uuid VARCHAR(500), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE sale_line_items ADD COLUMN gift_card_purchase_id VARCHAR(64) REFERENCES gift_card_purchases(id); diff --git a/db/up/remove_stripe_acct_id.sql b/db/up/remove_stripe_acct_id.sql new file mode 100644 index 000000000..5f44e52ba --- /dev/null +++ b/db/up/remove_stripe_acct_id.sql @@ -0,0 +1 @@ +ALTER TABLE teachers DROP COLUMN stripe_account_id; \ No newline at end of file diff --git a/db/up/reviews.sql b/db/up/reviews.sql new file mode 100644 index 000000000..89a30ee32 --- /dev/null +++ b/db/up/reviews.sql @@ -0,0 +1,23 @@ +CREATE TABLE reviews ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + target_id VARCHAR(64) NOT NULL, + target_type VARCHAR(32) NOT NULL, + description VARCHAR, + rating INT NOT NULL, + deleted_by_user_id VARCHAR(64) REFERENCES users(id) ON DELETE SET NULL, + deleted_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL +); + +CREATE TABLE review_summaries ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + target_id VARCHAR(64) NOT NULL, + target_type VARCHAR(32) NOT NULL, + avg_rating FLOAT NOT NULL, + wilson_score FLOAT NOT NULL, + review_count INT NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL +); \ No newline at end of file diff --git a/db/up/session_controller.sql b/db/up/session_controller.sql new file mode 100644 index 000000000..d146481b6 --- /dev/null +++ b/db/up/session_controller.sql @@ -0,0 +1 @@ +ALTER TABLE music_sessions ADD COLUMN session_controller_id VARCHAR(64) REFERENCES users(id); \ No newline at end of file diff --git a/db/up/teacher_complete.sql b/db/up/teacher_complete.sql new file mode 100644 index 000000000..7e67f1a20 --- /dev/null +++ b/db/up/teacher_complete.sql @@ -0,0 +1,2 @@ +ALTER TABLE teachers ADD COLUMN profile_pct NUMERIC(8,2) ; +ALTER TABLE teachers ADD COLUMN profile_pct_summary JSON; \ No newline at end of file diff --git a/db/up/teacher_progression.sql b/db/up/teacher_progression.sql new file mode 100644 index 000000000..111a453ee --- /dev/null +++ b/db/up/teacher_progression.sql @@ -0,0 +1,4 @@ +ALTER TABLE teachers ADD COLUMN background_check_at TIMESTAMP WITHOUT TIME ZONE; +ALTER TABLE teachers ADD COLUMN ready_for_session_at TIMESTAMP WITHOUT TIME ZONE; +ALTER TABLE users ADD COLUMN ready_for_session_at TIMESTAMP WITHOUT TIME ZONE; +ALTER TABLE teachers ADD COLUMN top_rated BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/db/up/teacher_student_flags.sql b/db/up/teacher_student_flags.sql new file mode 100644 index 000000000..c94290d2f --- /dev/null +++ b/db/up/teacher_student_flags.sql @@ -0,0 +1,2 @@ +ALTER TABLE users ADD COLUMN is_a_student BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE users ADD COLUMN is_a_teacher BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/db/up/test_drive_lessons.sql b/db/up/test_drive_lessons.sql new file mode 100644 index 000000000..bac4f76eb --- /dev/null +++ b/db/up/test_drive_lessons.sql @@ -0,0 +1,2 @@ +ALTER TABLE teachers ADD COLUMN test_drives_per_week INTEGER NOT NULL DEFAULT 2; +ALTER TABLE teachers ADD COLUMN teaches_test_drive BOOLEAN NOT NULL DEFAULT TRUE; \ No newline at end of file diff --git a/db/up/track_downloads.sql b/db/up/track_downloads.sql new file mode 100644 index 000000000..f328a9579 --- /dev/null +++ b/db/up/track_downloads.sql @@ -0,0 +1,30 @@ +CREATE TABLE download_trackers ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE, + remote_ip VARCHAR(400) NOT NULL, + jam_track_id VARCHAR (64) NOT NULL REFERENCES jam_tracks(id) ON DELETE CASCADE, + paid BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX index_download_trackers_on_user_id ON download_trackers USING btree (user_id); +CREATE INDEX index_download_trackers_on_remote_ip ON download_trackers USING btree (remote_ip); +CREATE INDEX index_download_trackers_on_created_at ON download_trackers USING btree (created_at, paid); + + +CREATE TABLE ip_blacklists ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + remote_ip VARCHAR(400) UNIQUE NOT NULL, + notes VARCHAR, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +CREATE TABLE user_blacklists ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) UNIQUE NOT NULL REFERENCES users(id) ON DELETE CASCADE, + notes VARCHAR, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/db/up/track_school_signups.sql b/db/up/track_school_signups.sql new file mode 100644 index 000000000..7e66f48fc --- /dev/null +++ b/db/up/track_school_signups.sql @@ -0,0 +1 @@ +ALTER TABLE USERS ADD COLUMN school_interest BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/db/up/track_user_on_lesson.sql b/db/up/track_user_on_lesson.sql new file mode 100644 index 000000000..a5e381d96 --- /dev/null +++ b/db/up/track_user_on_lesson.sql @@ -0,0 +1,4 @@ +ALTER TABLE lesson_sessions ADD COLUMN user_id VARCHAR(64) REFERENCES users(id); +UPDATE lesson_sessions SET user_id = (select user_id from music_sessions where lesson_sessions.id = music_sessions.lesson_session_id) where user_id is NULL; +ALTER TABLE lesson_sessions ALTER COLUMN user_id SET NOT NULL; +CREATE INDEX msuh_client_id ON music_sessions_user_history USING btree (client_id); \ No newline at end of file diff --git a/db/up/update_payment_history.sql b/db/up/update_payment_history.sql new file mode 100644 index 000000000..0e3e345ca --- /dev/null +++ b/db/up/update_payment_history.sql @@ -0,0 +1 @@ +ALTER TABLE charges ADD COLUMN user_id VARCHAR(64) REFERENCES users(id); \ No newline at end of file diff --git a/db/up/updated_subjects.sql b/db/up/updated_subjects.sql new file mode 100644 index 000000000..4bd7873e1 --- /dev/null +++ b/db/up/updated_subjects.sql @@ -0,0 +1,27 @@ +-- https://jamkazam.atlassian.net/browse/VRFS-3407 +UPDATE subjects SET description = 'Composition' WHERE id = 'composing'; +UPDATE subjects SET description = 'Recording & Production' WHERE id = 'recording'; +UPDATE subjects SET description = 'Sight Reading' WHERE id = 'site-reading'; + +INSERT INTO subjects(id, description) VALUES ('film-scoring', 'Film Scoring'); +INSERT INTO subjects(id, description) VALUES ('video-game-scoring', 'Video Game Scoring'); +INSERT INTO subjects(id, description) VALUES ('ear-training', 'Ear Training'); +INSERT INTO subjects(id, description) VALUES ('harmony', 'Harmony'); +INSERT INTO subjects(id, description) VALUES ('music-therapy', 'Music Therapy'); +INSERT INTO subjects(id, description) VALUES ('songwriting', 'Songwriting'); +INSERT INTO subjects(id, description) VALUES ('conducting', 'Conducting'); +INSERT INTO subjects(id, description) VALUES ('instrument-repair', 'Instrument Repair'); +INSERT INTO subjects(id, description) VALUES ('improvisation', 'Improvisation'); +INSERT INTO subjects(id, description) VALUES ('pro-tools', 'Pro Tools'); +INSERT INTO subjects(id, description) VALUES ('ableton-live', 'Ableton Live'); +INSERT INTO subjects(id, description) VALUES ('fl-studio', 'FL Studio'); +INSERT INTO subjects(id, description) VALUES ('garageband', 'GarageBand'); +INSERT INTO subjects(id, description) VALUES ('apple-logic-pro', 'Apple Logic Pro'); +INSERT INTO subjects(id, description) VALUES ('presonus-studio-one', 'PreSonus Studio One'); +INSERT INTO subjects(id, description) VALUES ('reaper', 'Reaper'); +INSERT INTO subjects(id, description) VALUES ('cubase', 'Cubase'); +INSERT INTO subjects(id, description) VALUES ('sonar', 'Sonar'); +INSERT INTO subjects(id, description) VALUES ('reason', 'Reason'); +INSERT INTO subjects(id, description) VALUES ('amplitube', 'AmpliTube'); +INSERT INTO subjects(id, description) VALUES ('line-6-pod', 'Line 6 Pod'); +INSERT INTO subjects(id, description) VALUES ('guitar-ring', 'Guitar Rig'); \ No newline at end of file diff --git a/db/up/user_origin.sql b/db/up/user_origin.sql new file mode 100644 index 000000000..f778b731f --- /dev/null +++ b/db/up/user_origin.sql @@ -0,0 +1,4 @@ +ALTER TABLE users ADD COLUMN origin_utm_source VARCHAR DEFAULT 'legacy'; +ALTER TABLE users ADD COLUMN origin_utm_medium VARCHAR; +ALTER TABLE users ADD COLUMN origin_utm_campaign VARCHAR; +ALTER TABLE users ADD COLUMN origin_referrer VARCHAR; diff --git a/db/up/versionable_jamtracks.sql b/db/up/versionable_jamtracks.sql new file mode 100644 index 000000000..9751bb940 --- /dev/null +++ b/db/up/versionable_jamtracks.sql @@ -0,0 +1 @@ +ALTER TABLE jam_track_rights ADD COLUMN version VARCHAR NOT NULL DEFAULT '0'; \ No newline at end of file diff --git a/db/up/video_recording.sql b/db/up/video_recording.sql new file mode 100644 index 000000000..46502b104 --- /dev/null +++ b/db/up/video_recording.sql @@ -0,0 +1,3 @@ +ALTER TABLE recordings ADD video BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE user_authorizations ADD refresh_token VARCHAR; +ALTER TABLE recordings ADD external_video_id VARCHAR; \ No newline at end of file diff --git a/db/up/web_playable_jamtracks.sql b/db/up/web_playable_jamtracks.sql new file mode 100644 index 000000000..b3d6e8a4e --- /dev/null +++ b/db/up/web_playable_jamtracks.sql @@ -0,0 +1,16 @@ +ALTER TABLE users ADD COLUMN first_opened_jamtrack_web_player TIMESTAMP; +ALTER TABLE jam_track_rights ADD COLUMN last_stem_id VARCHAR(64) REFERENCES jam_track_tracks(id) ON DELETE SET NULL; +ALTER TABLE jam_track_tracks ADD COLUMN url_mp3_48 VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN md5_mp3_48 VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN length_mp3_48 BIGINT; +ALTER TABLE jam_track_tracks ADD COLUMN url_aac_48 VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN md5_aac_48 VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN length_aac_48 BIGINT; + +CREATE TABLE user_events ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) REFERENCES users(id) ON DELETE SET NULL, + name VARCHAR(100) NOT NULL, + detail JSON, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/db/up/whitelist.sql b/db/up/whitelist.sql new file mode 100644 index 000000000..9bb1eb97e --- /dev/null +++ b/db/up/whitelist.sql @@ -0,0 +1,18 @@ + + +CREATE TABLE ip_whitelists ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + remote_ip VARCHAR(400) UNIQUE NOT NULL, + notes VARCHAR, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +CREATE TABLE user_whitelists ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) UNIQUE NOT NULL REFERENCES users(id) ON DELETE CASCADE, + notes VARCHAR, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/pb/build_rprotoc b/pb/build_rprotoc index e019cc519..f4c5555b1 100755 --- a/pb/build_rprotoc +++ b/pb/build_rprotoc @@ -5,10 +5,10 @@ RUBY_OUT=$TARGET/ruby # we exit with 0; treat ruby as optional at the moment -command -v "bundle" >/dev/null 2>&1 || { echo >&2 "bundle is required but not installed. Skipping ruby protocol buffers."; exit 0; } +command -v "bundle" || { echo >&2 "bundle is required but not installed. Skipping ruby protocol buffers."; exit 0; } # creates a bin folder with 'rprotoc' command inside -bundle install --binstubs > /dev/null +bundle install --binstubs # die on error at this point set -e diff --git a/pb/package_ruby b/pb/package_ruby index 46de2891d..282f4df38 100755 --- a/pb/package_ruby +++ b/pb/package_ruby @@ -5,7 +5,9 @@ pushd target/ruby > /dev/null rm -rf jampb -bundle gem jampb > /dev/null +#bundle gem jampb > /dev/null +echo "build gem jampb" +bundle gem jampb # copy over built ruby code cp src/*.rb jampb/lib/jampb @@ -57,7 +59,9 @@ end EOF +echo "gem build jampb.gemspec" gem build jampb.gemspec > /dev/null +#gem build jampb.gemspec > /dev/null popd > /dev/null popd > /dev/null diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index 5d5ecf100..1db3e557a 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -11,6 +11,9 @@ message ClientMessage { enum Type { LOGIN = 100; LOGIN_ACK = 105; + LOGOUT = 106; + LOGOUT_ACK = 107; + CONNECT_ACK = 108; LOGIN_MUSIC_SESSION = 110; LOGIN_MUSIC_SESSION_ACK = 115; LEAVE_MUSIC_SESSION = 120; @@ -21,6 +24,8 @@ message ClientMessage { UNSUBSCRIBE = 137; SUBSCRIPTION_MESSAGE = 138; SUBSCRIBE_BULK = 139; + USER_STATUS = 141; + DIAGNOSTIC = 142; // friend notifications FRIEND_UPDATE = 140; @@ -67,6 +72,7 @@ message ClientMessage { // text message TEXT_MESSAGE = 236; CHAT_MESSAGE = 237; + SEND_CHAT_MESSAGE = 238; MUSICIAN_SESSION_FRESH = 240; MUSICIAN_SESSION_STALE = 245; @@ -82,6 +88,13 @@ message ClientMessage { JAM_TRACK_SIGN_COMPLETE = 260; JAM_TRACK_SIGN_FAILED = 261; + // jamtracks mixdown notifications + MIXDOWN_SIGN_COMPLETE = 270; + MIXDOWN_SIGN_FAILED = 271; + + LESSON_MESSAGE = 280; + SCHEDULED_JAMCLASS_INVITATION = 281; + TEST_SESSION_MESSAGE = 295; PING_REQUEST = 300; @@ -96,6 +109,9 @@ message ClientMessage { RESTART_APPLICATION = 403; STOP_APPLICATION = 404; + // jamblaster messages + PAIR_ATTEMPT = 500; + SERVER_BAD_STATE_RECOVERED = 900; SERVER_GENERIC_ERROR = 1000; @@ -117,6 +133,9 @@ message ClientMessage { // Client-Server messages (to/from) optional Login login = 100; // to server optional LoginAck login_ack = 105; // from server + optional Logout logout = 106; // to server + optional LogoutAck logout_ack = 107; // from server + optional ConnectAck connect_ack = 108; // from server optional LoginMusicSession login_music_session = 110; // to server optional LoginMusicSessionAck login_music_session_ack = 115; // from server optional LeaveMusicSession leave_music_session = 120; @@ -127,6 +146,8 @@ message ClientMessage { optional Unsubscribe unsubscribe = 137; optional SubscriptionMessage subscription_message = 138; optional SubscribeBulk subscribe_bulk = 139; + optional UserStatus user_status = 141; + optional Diagnostic diagnostic = 142; // friend notifications optional FriendUpdate friend_update = 140; // from server to all friends of user @@ -174,6 +195,7 @@ message ClientMessage { // text message optional TextMessage text_message = 236; optional ChatMessage chat_message = 237; + optional SendChatMessage send_chat_message = 238; optional MusicianSessionFresh musician_session_fresh = 240; optional MusicianSessionStale musician_session_stale = 245; @@ -188,6 +210,13 @@ message ClientMessage { optional JamTrackSignComplete jam_track_sign_complete = 260; optional JamTrackSignFailed jam_track_sign_failed = 261; + // jamtrack mixdown notification + optional MixdownSignComplete mixdown_sign_complete = 270; + optional MixdownSignFailed mixdown_sign_failed = 271; + + // lesson notifications + optional LessonMessage lesson_message = 280; + optional ScheduledJamclassInvitation scheduled_jamclass_invitation = 281; // Client-Session messages (to/from) optional TestSessionMessage test_session_message = 295; @@ -205,6 +234,9 @@ message ClientMessage { optional RestartApplication restart_application = 403; optional StopApplication stop_application = 404; + // JamBlaster messages + optional PairAttempt pair_attempt = 500; + // Server-to-Client special messages optional ServerBadStateRecovered server_bad_state_recovered = 900; @@ -243,8 +275,31 @@ message LoginAck { optional string user_id = 7; // the database user id optional int32 connection_expire_time = 8; // this is how long the server gives you before killing your connection entirely after missing heartbeats optional ClientUpdate client_update = 9; + optional string username = 10; } +message ConnectAck { + optional string public_ip = 1; + optional string client_id = 2; // a new client_id if none is supplied in Login, or just the original client_id echoed back + optional int32 heartbeat_interval = 3; // set your heartbeat interval to this value + optional int32 connection_expire_time = 4; // this is how long the server gives you before killing your connection entirely after missing heartbeats + optional ClientUpdate client_update = 5; +} + +// route_to: server +// a logout ack is always sent +message Logout { + +} + +// route_to: client +// sent from server to client to let the client know the login was successful, +// and to also show the IP address of the client as seen by the server. +message LogoutAck { + optional bool success = 1; +} + + // route_to: server // send from client to server to log in to a music session and be 'present'. // if successful, a LoginMusicSessionAck is sent with error = false, and Client-Session messages will be passed @@ -355,6 +410,7 @@ message SessionJoin { optional string msg = 3; optional int32 track_changes_counter = 4; optional string source_user_id = 5; + optional string client_id = 6; } message SessionDepart { @@ -363,6 +419,8 @@ message SessionDepart { optional string msg = 3; optional string recording_id = 4; optional int32 track_changes_counter = 5; + optional string client_id = 6; + optional string source_user_id = 7; } message TracksChanged { @@ -560,6 +618,18 @@ message ChatMessage { optional string msg = 3; optional string msg_id = 4; optional string created_at = 5; + optional string channel = 6; + optional string lesson_session_id = 7; + optional string purpose = 8; + optional string attachment_id = 9; + optional string attachment_type = 10; + optional string attachment_name = 11; + +} + +message SendChatMessage { + optional string msg = 1; + optional string channel = 2; } // route_to: client: @@ -612,6 +682,40 @@ message JamTrackSignFailed { required int32 jam_track_right_id = 1; // jam track right id } +message MixdownSignComplete { + required string mixdown_package_id = 1; // jam track mixdown package id +} + +message MixdownSignFailed { + required string mixdown_package_id = 1; // jam track mixdown package id +} + +message LessonMessage { + optional string music_session_id = 1; + optional string photo_url = 2; + optional string msg = 3; + optional string notification_id = 4; + optional string created_at = 5; + optional string sender_id = 6; + optional string receiver_id = 7; + optional bool student_directed = 8; + optional string purpose = 9; + optional string sender_name = 10; + optional string lesson_session_id = 11; +} + +message ScheduledJamclassInvitation { + optional string session_id = 1; + optional string photo_url = 2; + optional string msg = 3; + optional string session_name = 4; + optional string session_date = 5; + optional string notification_id = 6; + optional string created_at = 7; + optional string lesson_session_id = 8; +} + + message SubscriptionMessage { optional string type = 1; // the type of the subscription optional string id = 2; // data about what to subscribe to, specifically @@ -639,6 +743,16 @@ message SubscribeBulk { //repeated Subscription subscriptions = 1; # the ruby protocol buffer library chokes on this. so we have to do the above } +message UserStatus { + optional bool active = 1; // same as heartbeat 'active'... does the user appear present + optional string status = 2; +} + + +message Diagnostic { + optional string message = 1; +} + // route_to: session // a test message used by ruby-client currently. just gives way to send out to rest of session message TestSessionMessage { @@ -675,6 +789,7 @@ message TestClientMessage { message Heartbeat { optional string notification_seen = 1; optional string notification_seen_at = 2; + optional bool active = 3; // is the user active? } // target: client @@ -716,6 +831,11 @@ message StopApplication { } +message PairAttempt { + optional string scid = 1; + optional string vtoken = 2; +} + // route_to: client // this should follow a ServerBadStateError in the case that the // websocket gateway recovers from whatever ailed it diff --git a/ruby/Gemfile b/ruby/Gemfile index b5c229e8e..e1b13151c 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -32,6 +32,7 @@ gem 'bcrypt-ruby', '3.0.1' gem 'ruby-protocol-buffers', '1.2.2' gem 'eventmachine', '1.0.4' gem 'amqp', '1.0.2' +gem 'kickbox' gem 'will_paginate' gem 'sendgrid', '1.2.0' gem 'aws-sdk', '~> 1' @@ -54,8 +55,13 @@ gem 'rest-client' gem 'iso-639' gem 'rubyzip' gem 'sanitize' -gem 'influxdb', '0.1.8' +#gem 'influxdb' gem 'recurly' +gem 'sendgrid_toolkit', '>= 1.1.1' +gem 'stripe' +gem 'zip-codes' +gem 'icalendar' +gem 'email_validator' group :test do gem 'simplecov', '~> 0.7.1' @@ -70,7 +76,8 @@ group :test do gem 'rspec-prof' gem 'time_difference' gem 'byebug' - gem 'icalendar' + gem 'stripe-ruby-mock' + end # Specify your gem's dependencies in jam_ruby.gemspec diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 4e21009ce..45fc7b857 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -26,15 +26,19 @@ require 'rest-client' require 'zip' require 'csv' require 'tzinfo' +require 'stripe' +require 'zip-codes' +require 'email_validator' ActiveRecord::Base.raise_in_transactional_callbacks = true - +require "jam_ruby/lib/timezone" require "jam_ruby/constants/limits" require "jam_ruby/constants/notification_types" require "jam_ruby/constants/validation_messages" require "jam_ruby/errors/jam_permission_error" require "jam_ruby/errors/state_error" require "jam_ruby/errors/jam_argument_error" +require "jam_ruby/errors/jam_record_not_found" require "jam_ruby/errors/conflict_error" require "jam_ruby/lib/app_config" require "jam_ruby/lib/s3_manager_mixin" @@ -59,6 +63,8 @@ require "jam_ruby/resque/scheduled/cleanup_facebook_signup" require "jam_ruby/resque/scheduled/unused_music_notation_cleaner" require "jam_ruby/resque/scheduled/user_progress_emailer" require "jam_ruby/resque/scheduled/daily_job" +require "jam_ruby/resque/scheduled/hourly_job" +require "jam_ruby/resque/scheduled/minutely_job" require "jam_ruby/resque/scheduled/daily_session_emailer" require "jam_ruby/resque/scheduled/new_musician_emailer" require "jam_ruby/resque/scheduled/music_session_reminder" @@ -71,6 +77,7 @@ require "jam_ruby/resque/scheduled/jam_tracks_cleaner" 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/jam_track_mixdown_packager" require "jam_ruby/resque/google_analytics_event" require "jam_ruby/resque/batch_email_job" require "jam_ruby/resque/long_running" @@ -112,12 +119,21 @@ require "jam_ruby/models/max_mind_release" require "jam_ruby/models/genre_player" require "jam_ruby/models/genre" require "jam_ruby/models/user" +require "jam_ruby/models/user_event" 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/download_tracker" +require "jam_ruby/models/ip_blacklist" +require "jam_ruby/models/user_blacklist" +require "jam_ruby/models/email_blacklist" +require "jam_ruby/models/ip_whitelist" +require "jam_ruby/models/user_whitelist" require "jam_ruby/models/fraud_alert" require "jam_ruby/models/fingerprint_whitelist" +require "jam_ruby/models/review" +require "jam_ruby/models/review_summary" require "jam_ruby/models/rsvp_request" require "jam_ruby/models/rsvp_slot" require "jam_ruby/models/rsvp_request_rsvp_slot" @@ -216,6 +232,8 @@ require "jam_ruby/models/jam_track_track" require "jam_ruby/models/jam_track_right" #require "jam_ruby/models/jam_track_tap_in" # consider deletion require "jam_ruby/models/jam_track_file" +require "jam_ruby/models/jam_track_mixdown" +require "jam_ruby/models/jam_track_mixdown_package" require "jam_ruby/models/genre_jam_track" require "jam_ruby/app/mailers/async_mailer" require "jam_ruby/app/mailers/batch_mailer" @@ -236,11 +254,16 @@ 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/news" require "jam_ruby/models/sale" require "jam_ruby/models/sale_line_item" require "jam_ruby/models/recurly_transaction_web_hook" require "jam_ruby/models/broadcast_notification" require "jam_ruby/models/broadcast_notification_view" +require "jam_ruby/models/test_drive_package" +require "jam_ruby/models/test_drive_package_teacher" +require "jam_ruby/models/test_drive_package_choice" +require "jam_ruby/models/test_drive_package_choice_teacher" require "jam_ruby/calendar_manager" require "jam_ruby/jam_tracks_manager" require "jam_ruby/jam_track_importer" @@ -250,9 +273,44 @@ require "jam_ruby/models/online_presence" require "jam_ruby/models/json_store" require "jam_ruby/models/base_search" require "jam_ruby/models/musician_search" +require "jam_ruby/models/teacher" +require "jam_ruby/models/teacher_experience" +require "jam_ruby/models/language" +require "jam_ruby/models/subject" require "jam_ruby/models/band_search" require "jam_ruby/import/tency_stem_mapping" - +require "jam_ruby/models/jam_track_search" +require "jam_ruby/models/gift_card" +require "jam_ruby/models/gift_card_purchase" +require "jam_ruby/models/gift_card_type" +require "jam_ruby/models/jam_track_session" +require "jam_ruby/models/lesson_package_type" +require "jam_ruby/models/lesson_package_purchase" +require "jam_ruby/models/lesson_session" +require "jam_ruby/models/lesson_booking" +require "jam_ruby/models/lesson_booking_slot" +require "jam_ruby/models/jamblaster" +require "jam_ruby/models/jamblaster_user" +require "jam_ruby/models/jamblaster_pairing_request" +require "jam_ruby/models/sale_receipt_ios" +require "jam_ruby/models/lesson_session_analyser" +require "jam_ruby/models/lesson_session_monthly_price" +require "jam_ruby/models/teacher_distribution" +require "jam_ruby/models/teacher_payment" +require "jam_ruby/models/charge" +require "jam_ruby/models/teacher_payment_charge" +require "jam_ruby/models/affiliate_payment_charge" +require "jam_ruby/models/lesson_payment_charge" +require "jam_ruby/models/affiliate_distribution" +require "jam_ruby/models/teacher_intent" +require "jam_ruby/models/school" +require "jam_ruby/models/school_invitation" +require "jam_ruby/models/teacher_instrument" +require "jam_ruby/models/teacher_subject" +require "jam_ruby/models/teacher_language" +require "jam_ruby/models/teacher_genre" +require "jam_ruby/models/jam_class_report" +require "jam_ruby/models/campaign_spend" include Jampb module JamRuby diff --git a/ruby/lib/jam_ruby/app/assets/sounds/knock44.wav b/ruby/lib/jam_ruby/app/assets/sounds/knock44.wav new file mode 100644 index 000000000..33c1aea5f Binary files /dev/null and b/ruby/lib/jam_ruby/app/assets/sounds/knock44.wav differ diff --git a/ruby/lib/jam_ruby/app/assets/sounds/knock48.wav b/ruby/lib/jam_ruby/app/assets/sounds/knock48.wav new file mode 100644 index 000000000..3a3fd3d68 Binary files /dev/null and b/ruby/lib/jam_ruby/app/assets/sounds/knock48.wav differ diff --git a/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb index f14d8e424..993b9f81b 100644 --- a/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb @@ -1,6 +1,6 @@ module JamRuby # sends out a boring ale - class AdminMailer < ActionMailer::Base + class AdminMailer < ActionMailer::Base include SendGrid @@ -20,6 +20,30 @@ module JamRuby subject: options[:subject]) end + def crash_alert(options) + mail(to: APP_CONFIG.email_crashes_alias, + from: APP_CONFIG.email_generic_from, + body: options[:body], + content_type: "text/plain", + subject: options[:subject]) + end + + def social(options) + mail(to: APP_CONFIG.email_social_alias, + from: options[:from] || APP_CONFIG.email_generic_from, + body: options[:body], + content_type: "text/plain", + subject: options[:subject]) + end + + def partner(options) + mail(to: APP_CONFIG.email_partners_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] diff --git a/ruby/lib/jam_ruby/app/mailers/auto_mailer.rb b/ruby/lib/jam_ruby/app/mailers/auto_mailer.rb new file mode 100644 index 000000000..05f144821 --- /dev/null +++ b/ruby/lib/jam_ruby/app/mailers/auto_mailer.rb @@ -0,0 +1,23 @@ +module JamRuby + # UserMailer must be configured to work + # Some common configs occur in jam_ruby/init.rb + # Environment specific configs occur in spec_helper.rb in jam-ruby and jam-web (to put it into test mode), + # and in config/initializers/email.rb in rails to configure sendmail account settings + # If UserMailer were to be used in another project, it would need to be configured there, as well. + + # Templates for UserMailer can be found in jam_ruby/app/views/jam_ruby/user_mailer + class UserMailer < ActionMailer::Base + include SendGrid + + layout "auto_mailer" + + 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 + + end +end diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb index 40d5480f0..b5f055419 100644 --- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb @@ -1,606 +1,1797 @@ - module JamRuby - # UserMailer must be configured to work - # Some common configs occur in jam_ruby/init.rb - # Environment specific configs occur in spec_helper.rb in jam-ruby and jam-web (to put it into test mode), - # and in config/initializers/email.rb in rails to configure sendmail account settings - # If UserMailer were to be used in another project, it would need to be configured there, as well. +module JamRuby + # UserMailer must be configured to work + # Some common configs occur in jam_ruby/init.rb + # Environment specific configs occur in spec_helper.rb in jam-ruby and jam-web (to put it into test mode), + # and in config/initializers/email.rb in rails to configure sendmail account settings + # If UserMailer were to be used in another project, it would need to be configured there, as well. - # Templates for UserMailer can be found in jam_ruby/app/views/jam_ruby/user_mailer - class UserMailer < ActionMailer::Base - include SendGrid + # Templates for UserMailer can be found in jam_ruby/app/views/jam_ruby/user_mailer + class UserMailer < ActionMailer::Base + include SendGrid - layout "user_mailer" + layout "user_mailer" - DEFAULT_SENDER = "JamKazam " + DEFAULT_SENDER = "JamKazam " - default :from => DEFAULT_SENDER + 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 + sendgrid_category :use_subject_lines + #sendgrid_enable :opentrack, :clicktrack # this makes our emails creepy, imo (seth) + sendgrid_unique_args :env => Environment.mode - def confirm_email(user, signup_confirm_url) - @user = user - @signup_confirm_url = signup_confirm_url - sendgrid_category "Confirm Email" - sendgrid_unique_args :type => "confirm_email" + def confirm_email(user, signup_confirm_url) + @user = user + @signup_confirm_url = signup_confirm_url + sendgrid_category "Confirm Email" + sendgrid_unique_args :type => "confirm_email" - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) - mail(:to => user.email, :subject => "Please confirm your JamKazam email") do |format| - format.text - format.html + mail(:to => user.email, :subject => "Please confirm your JamKazam email") do |format| + format.text + format.html + end + end + + def welcome_message(user) + @user = user + sendgrid_category "Welcome" + sendgrid_unique_args :type => "welcome_message" + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name]) + + mail(:to => user.email, :subject => "Welcome to JamKazam") do |format| + format.text + format.html + end + end + + def student_welcome_message(user) + @user = user + @subject = "Welcome to JamKazam and JamClass online lessons!" + sendgrid_category "Welcome" + sendgrid_unique_args :type => "welcome_message" + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name]) + + mail(:to => user.email, :subject => @subject) do |format| + format.text + format.html + end + end + + def teacher_welcome_message(user) + @user = user + @subject= "Welcome to JamKazam and JamClass online lessons!" + sendgrid_category "Welcome" + sendgrid_unique_args :type => "welcome_message" + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name]) + + mail(:to => user.email, :subject => @subject) do |format| + format.text + format.html + end + end + + def school_owner_welcome_message(user) + @user = user + @subject= "Welcome to JamKazam and JamClass online lessons!" + sendgrid_category "Welcome" + sendgrid_unique_args :type => "welcome_message" + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name]) + + mail(:to => user.email, :subject => @subject) do |format| + format.text + format.html + end + end + + def password_changed(user) + @user = user + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + + sendgrid_unique_args :type => "password_changed" + mail(:to => user.email, :subject => "JamKazam Password Changed") do |format| + format.text + format.html + end + end + + def password_reset(user, password_reset_url) + @user = user + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + + @password_reset_url = password_reset_url + sendgrid_unique_args :type => "password_reset" + mail(:to => user.email, :subject => "JamKazam Password Reset") do |format| + format.text + format.html + end + end + + def updating_email(user) + @user = user + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + + sendgrid_unique_args :type => "updating_email" + mail(:to => user.update_email, :subject => "JamKazam Email Change Confirmation") do |format| + format.text + format.html + end + end + + def updated_email(user) + @user = user + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + + sendgrid_unique_args :type => "updated_email" + mail(:to => user.email, :subject => "JamKazam Email Changed") do |format| + format.text + format.html + end + end + + def new_musicians(user, new_musicians, host='www.jamkazam.com') + @user, @new_musicians, @host = user, new_musicians, host + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + sendgrid_unique_args :type => "new_musicians" + + mail(:to => user.email, :subject => EmailBatchNewMusician.subject) do |format| + format.text + format.html + end + end + + #################################### NOTIFICATION EMAILS #################################### + def friend_request(user, msg, friend_request_id) + return if !user.subscribe_email + + email = user.email + subject = "You have a new friend request on JamKazam" + unique_args = {:type => "friend_request"} + + @url = Nav.accept_friend_request_dialog(friend_request_id) + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def friend_request_accepted(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "You have a new friend on JamKazam" + unique_args = {:type => "friend_request_accepted"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def new_user_follower(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "You have a new follower on JamKazam" + unique_args = {:type => "new_user_follower"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def new_band_follower(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "Your band has a new follower on JamKazam" + unique_args = {:type => "new_band_follower"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def session_invitation(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "You have been invited to a session on JamKazam" + unique_args = {:type => "session_invitation"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def musician_session_join(user, msg, session_id) + return if !user.subscribe_email + + email = user.email + subject = "Someone you know is in a session on JamKazam" + unique_args = {:type => "musician_session_join"} + @body = msg + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_invitation(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Session Invitation" + unique_args = {:type => "scheduled_session_invitation"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_rsvp(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Session RSVP" + unique_args = {:type => "scheduled_session_rsvp"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_rsvp_approved(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Session RSVP Approved" + unique_args = {:type => "scheduled_session_rsvp_approved"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_rsvp_cancelled(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Session RSVP Cancelled" + unique_args = {:type => "scheduled_session_rsvp_cancelled"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_rsvp_cancelled_org(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Your Session RSVP Cancelled" + unique_args = {:type => "scheduled_session_rsvp_cancelled_org"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_cancelled(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Session Cancelled" + unique_args = {:type => "scheduled_session_cancelled"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_rescheduled(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Session Rescheduled" + unique_args = {:type => "scheduled_session_rescheduled"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_reminder_upcoming(user, session) + subject = "Your JamKazam session starts in 1 hour!" + unique_args = {:type => "scheduled_session_reminder_upcoming"} + send_scheduled_session_reminder(user, session, subject, unique_args) + end + + def scheduled_session_reminder_day(user, session) + subject = "JamKazam Session Reminder" + unique_args = {:type => "scheduled_session_reminder_day"} + send_scheduled_session_reminder(user, session, subject, unique_args) + end + + def send_scheduled_session_reminder(user, session, subject, unique_args) + return if !user.subscribe_email + + email = user.email + @user = user + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_comment(target_user, sender, msg, comment, session) + return if !target_user.subscribe_email + + email = target_user.email + subject = "New Session Comment" + unique_args = {:type => "scheduled_session_comment"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @comment = comment + @sender = sender + @suppress_user_has_account_footer = true + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [target_user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def scheduled_session_daily(receiver, sessions_and_latency) + sendgrid_category "Notification" + sendgrid_unique_args :type => "scheduled_session_daily" + + sendgrid_recipients([receiver.email]) + sendgrid_substitute('@USERID', [receiver.id]) + + @user = receiver + @sessions_and_latency = sessions_and_latency + + @title = 'New Scheduled Sessions Matched to You' + mail(:to => receiver.email, + :subject => EmailBatchScheduledSessions.subject) do |format| + format.text + format.html + end + end + + def band_session_join(user, msg, session_id) + return if !user.subscribe_email + + email = user.email + subject = "A band that you follow has joined a session" + unique_args = {:type => "band_session_join"} + + @body = msg + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def musician_recording_saved(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "A musician has saved a new recording on JamKazam" + unique_args = {:type => "musician_recording_saved"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def band_recording_saved(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "A band has saved a new recording on JamKazam" + unique_args = {:type => "band_recording_saved"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def band_invitation(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "You have been invited to join a band on JamKazam" + unique_args = {:type => "band_invitation"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def band_invitation_accepted(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "Your band invitation was accepted" + unique_args = {:type => "band_invitation_accepted"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + + def text_message(user, sender_id, sender_name, sender_photo_url, message) + return if !user.subscribe_email + + email = user.email + subject = "Message from #{sender_name}" + unique_args = {:type => "text_message"} + + @note = message + @url = Nav.home(dialog: 'text-message', dialog_opts: {d1: sender_id}) + @sender_id = sender_id + @sender_name = sender_name + @sender_photo_url = sender_photo_url + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def student_lesson_request(lesson_booking) + email = lesson_booking.user.email + subject = "You have sent a lesson request to #{lesson_booking.teacher.name}!" + unique_args = {:type => "student_lesson_request"} + + @sender = lesson_booking.teacher + @lesson_booking = lesson_booking + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [lesson_booking.user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + # teacher + def teacher_lesson_request(lesson_booking) + email = lesson_booking.school_over_teacher + subject = "You have received a lesson request through JamKazam!" + unique_args = {:type => "teacher_lesson_request"} + + @sender = lesson_booking.user + @lesson_booking = lesson_booking + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients(email) + sendgrid_substitute('@USERID', lesson_booking.school_over_teacher_ids) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def student_lesson_accepted(lesson_session, message, slot) + @slot = slot + if slot.is_teacher_approved? + @target = lesson_session.student + @sender = lesson_session.teacher + @subject = "Your lesson request is confirmed!" + else + @target = lesson_session.teacher + @sender = lesson_session.student + @subject = "Your have confirmed a lesson!" + end + @lesson_session = lesson_session + @message = message + email = lesson_session.student.email + unique_args = {:type => "student_lesson_accepted"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [lesson_session.student.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_lesson_accepted(lesson_session, message, slot) + @slot = slot + if slot.is_teacher_approved? + @target = lesson_session.student + @sender = lesson_session.teacher + @subject = "You have confirmed a lesson!" + else + @target = lesson_session.teacher + @sender = lesson_session.student + @subject = "Your lesson time change is confirmed by #{lesson_session.student.name}!" + end + + @lesson_session = lesson_session + @message = message + email = lesson_session.school_and_teacher + unique_args = {:type => "teacher_lesson_accepted"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients(email) + sendgrid_substitute('@USERID', lesson_session.school_and_teacher_ids) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def student_lesson_update_all(lesson_session, message, slot) + @slot = slot + if slot.is_teacher_created? + @target = lesson_session.teacher + @sender = lesson_session.student + subject = "All lesson times changed with #{lesson_session.teacher.name}!" + else + @target = lesson_session.student + @sender = lesson_session.teacher + subject = "All lesson times changed with #{lesson_session.teacher.name}!" + end + @lesson_session = lesson_session + @message = message + email = lesson_session.student.email + unique_args = {:type => "student_lesson_accepted"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [lesson_session.student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_lesson_update_all(lesson_session, message, slot) + @slot = slot + if slot.is_teacher_created? + @target = lesson_session.teacher + @sender = lesson_session.student + subject = "All lesson times changed with #{lesson_session.student.name}!" + else + @target = lesson_session.student + @sender = lesson_session.teacher + subject = "All lesson times changed with #{lesson_session.student.name}!" + end + + @lesson_session = lesson_session + @message = message + email = lesson_session.school_and_teacher + unique_args = {:type => "teacher_lesson_update_all"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients(email) + sendgrid_substitute('@USERID', lesson_session.school_and_teacher_ids) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_scheduled_jamclass_invitation(user, msg, session) + + email = user.email + @subject = "#{session.lesson_session.lesson_booking.display_type2.capitalize} JamClass Scheduled with #{session.lesson_session.student.name}" + unique_args = {:type => "scheduled_jamclass_invitation"} + @student = session.lesson_session.student + @teacher = session.lesson_session.teacher + @body = msg + @session_name = session.name + @session_description = session.description + @session_date = session.pretty_scheduled_start(true) + @session_url = session.lesson_session.web_url + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html + end + end + + def student_scheduled_jamclass_invitation(user, msg, session) + return if !user.subscribe_email + + email = user.email + @subject = "#{session.lesson_session.lesson_booking.display_type2.capitalize} JamClass Scheduled with #{session.lesson_session.teacher.name}" + unique_args = {:type => "scheduled_jamclass_invitation"} + @student = session.lesson_session.student + @teacher = session.lesson_session.teacher + @body = msg + @session_name = session.name + @session_description = session.description + @session_date = session.pretty_scheduled_start(true) + @session_url = session.lesson_session.web_url + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html + end + end + + # teacher proposed counter time; so send msg to the student + def student_lesson_counter(lesson_session, slot) + + email = lesson_session.student.email + subject = "Instructor has proposed a different time for your lesson" + unique_args = {:type => "student_lesson_counter"} + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + # student proposed counter time; so send msg to the teacher + def teacher_lesson_counter(lesson_session, slot) + + email = lesson_session.school_over_teacher + subject = "Student has proposed a different time for their lesson" + unique_args = {:type => "teacher_lesson_counter"} + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients(email) + sendgrid_substitute('@USERID', lesson_session.school_over_teacher_ids) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_lesson_completed(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + email = lesson_session.school_and_teacher + if @lesson_session.student_missed + subject = "You will be paid for your lesson with #{@student.name}" + else + subject = "You successfully completed a lesson with #{@student.name}" + end + + + unique_args = {:type => "teacher_lesson_completed"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients(email) + sendgrid_substitute('@USERID', lesson_session.school_and_teacher_ids) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + # successfully completed, and has some remaining test drives + def student_test_drive_lesson_completed(lesson_session) + + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + + email = @student.email + subject = "You have used #{@student.used_test_drives} of #{@student.total_test_drives} TestDrive lesson credits" + unique_args = {:type => "student_test_drive_success"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_test_drive_no_bill(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + + email = lesson_session.school_and_teacher + subject = "Your TestDrive with #{@student.name} was not successful" + unique_args = {:type => "teacher_test_drive_no_bill"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients(email) + sendgrid_substitute('@USERID', lesson_session.school_and_teacher_ids) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def student_test_drive_no_bill(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + + email = @student.email + subject = "Your TestDrive with #{@teacher.name} will not use a credit" + unique_args = {:type => "student_test_drive_no_bill"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + # successfully completed, but no more test drives left + def student_test_drive_lesson_done(lesson_session) + + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + + email = @student.email + subject = "You have used all TestDrive lesson credits" + unique_args = {:type => "student_test_drive_success"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "raw_mailer" } + end + end + + def student_lesson_normal_no_bill(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + + email = @student.email + subject = "Your lesson with #{@teacher.name} will not be billed" + unique_args = {:type => "student_lesson_normal_no_bill"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_lesson_normal_no_bill(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + email = lesson_session.school_and_teacher + subject = "Your student #{@student.name} will not be charged for their lesson" + unique_args = {:type => "teacher_lesson_normal_no_bill"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients(email) + sendgrid_substitute('@USERID', lesson_session.school_and_teacher_ids) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def student_lesson_normal_done(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + + email = @student.email + subject = "Your JamClass lesson today with #{@teacher.first_name}" + unique_args = {:type => "student_lesson_normal_done"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_lesson_normal_done(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + email = lesson_session.school_over_teacher + subject = "Your JamClass lesson today with #{@student.first_name}" + unique_args = {:type => "teacher_lesson_normal_done"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients(email) + sendgrid_substitute('@USERID', lesson_session.school_over_teacher_ids) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def student_unable_charge(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + @card_declined = lesson_session.is_card_declined? + @card_expired = lesson_session.is_card_expired? + @bill_date = lesson_session.last_billed_at_date + + email = @student.email + subject = "The credit card charge for your lesson today with #{@teacher.name} failed" + unique_args = {:type => "student_unable_charge"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_unable_charge(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + email = lesson_session.teacher.email + subject = "The credit card charge for your lesson today with #{@teacher.name} failed" + unique_args = {:type => "teacher_lesson_normal_done"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def student_unable_charge_monthly(lesson_package_purchase) + lesson_booking = lesson_package_purchase.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @lesson_package_purchase = lesson_package_purchase + @card_declined = lesson_package_purchase.is_card_declined? + @card_expired = lesson_package_purchase.is_card_expired? + @bill_date = lesson_package_purchase.last_billed_at_date + @lesson_booking = lesson_booking + @month_name = lesson_package_purchase.month_name + email = @student.email + if lesson_booking.is_suspended? + @subject = "Your weekly lessons with #{@teacher.name} have been suspended." + else + @subject = "The credit card charge for your #{@month_name} lessons with #{@teacher.name} failed." + end + + + unique_args = {:type => "student_unable_charge_monthly"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_unable_charge_monthly(lesson_package_purchase) + lesson_booking = lesson_package_purchase.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @lesson_package_purchase = lesson_package_purchase + @card_declined = lesson_package_purchase.is_card_declined? + @card_expired = lesson_package_purchase.is_card_expired? + @bill_date = lesson_package_purchase.last_billed_at_date + @lesson_booking = lesson_booking + @month_name = lesson_package_purchase.month_name + + email = lesson_booking.school_over_teacher + if lesson_booking.is_suspended? + @subject = "Your weekly lessons with #{@student.name} has been suspended." + else + @subject = "The student #{@student.name} had a failed credit card charge for #{@month_name}." + end + + + unique_args = {:type => "teacher_unable_charge_monthly"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients(email) + sendgrid_substitute('@USERID', lesson_booking.school_over_teacher_ids) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def student_lesson_monthly_charged(lesson_package_purchase) + lesson_booking = lesson_package_purchase.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @lesson_package_purchase = lesson_package_purchase + @card_declined = lesson_package_purchase.is_card_declined? + @card_expired = lesson_package_purchase.is_card_expired? + @bill_date = lesson_package_purchase.last_billed_at_date + @lesson_booking = lesson_booking + @month_name = lesson_package_purchase.month_name + email = @student.email + @subject = "Your JamClass lessons with #{@teacher.first_name} for #{@month_name}" + + unique_args = {:type => "student_lesson_monthly_charged"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_lesson_monthly_charged(lesson_package_purchase) + lesson_booking = lesson_package_purchase.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @lesson_package_purchase = lesson_package_purchase + @card_declined = lesson_package_purchase.is_card_declined? + @card_expired = lesson_package_purchase.is_card_expired? + @bill_date = lesson_package_purchase.last_billed_at_date + @lesson_booking = lesson_booking + @month_name = lesson_package_purchase.month_name + + email = lesson_booking.school_over_teacher + if lesson_booking.is_suspended? + @subject = "Your weekly lessons with #{@student.name} has been suspended." + else + @subject = "The student #{@student.name} had a failed credit card charge for #{@month_name}." + end + + + unique_args = {:type => "student_lesson_monthly_charged"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients(email) + sendgrid_substitute('@USERID', lesson_booking.school_over_teacher_ids) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + # always goes to the teacher + def teacher_distribution_done(teacher_payment) + @school = teacher_payment.school + @teacher_payment = teacher_payment + @distribution = teacher_payment.teacher_distribution + @teacher = teacher_payment.teacher + @payable_teacher = teacher_payment.payable_teacher + @name = @teacher.first_name || 'Anonymous' + @student = @distribution.student + email = @distribution.target.lesson_booking.school_over_teacher + + if @school + if @distribution.is_test_drive? + @subject = "Your TestDrive lesson with #{@student.name}" + elsif @distribution.is_normal? + @subject = "Your lesson with #{@student.name}" + elsif @distribution.is_monthly? + @subject = "Your #{@distribution.month_name} lessons with #{@student.name}" + else + @subject = "Your lesson with #{@student.name}" + end + else + if @distribution.is_test_drive? + @subject = "You have earned #{@distribution.real_distribution_display} for your TestDrive lesson with #{@student.first_name}" + elsif @distribution.is_normal? + @subject = "You have earned #{@distribution.real_distribution_display} for your lesson with #{@student.first_name}" + elsif @distribution.is_monthly? + @subject = "You have earned #{@distribution.real_distribution_display} for your #{@distribution.month_name} lessons with #{@student.first_name}" + else + @subject = "You have earned #{@distribution.real_distribution_display} for your lesson with #{@student.first_name}" end end - def welcome_message(user) - @user = user - sendgrid_category "Welcome" - sendgrid_unique_args :type => "welcome_message" + unique_args = {:type => "teacher_distribution_done"} - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) - sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - mail(:to => user.email, :subject => "Welcome to JamKazam") do |format| - format.text - format.html - end + sendgrid_recipients(email) + sendgrid_substitute('@USERID',@distribution.target.lesson_booking.school_over_teacher_ids ) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html end + end - def password_changed(user) - @user = user + # if school, goes to school owner; otherwise goes to teacher + def teacher_distribution_fail(teacher_payment) + @school = teacher_payment.school + @teacher_payment = teacher_payment + @distribution = teacher_payment.teacher_distribution + @teacher = teacher_payment.teacher + @payable_teacher = teacher_payment.payable_teacher + @student = @distribution.student + @name = @payable_teacher.first_name || 'Anonymous' + email = @distribution.target.lesson_booking.school_over_teacher - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) + @card_declined = teacher_payment.is_card_declined? + @card_expired = teacher_payment.is_card_expired? + @bill_date = teacher_payment.last_billed_at_date - sendgrid_unique_args :type => "password_changed" - mail(:to => user.email, :subject => "JamKazam Password Changed") do |format| - format.text - format.html - end - end - def password_reset(user, password_reset_url) - @user = user - - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) - - @password_reset_url = password_reset_url - sendgrid_unique_args :type => "password_reset" - mail(:to => user.email, :subject => "JamKazam Password Reset") do |format| - format.text - format.html + if @school + if @distribution.is_test_drive? + @subject = "We had a problem paying #{@distribution.real_distribution_display} for #{@teacher.name}'s TestDrive lesson with #{@student.name}" + elsif @distribution.is_normal? + @subject = "We had a problem paying #{@distribution.real_distribution_display} for #{@teacher.name}'s lesson with #{@student.name}" + elsif @distribution.is_monthly? + @subject = "We had a problem paying #{@distribution.real_distribution_display} for #{@teacher.name}'s #{@distribution.month_name} lessons with #{@student.name}" + else + @subject = "We had a problem paying #{@distribution.real_distribution_display} for #{@teacher.name}'s lesson with #{@student.name}" + end + else + if @distribution.is_test_drive? + @subject = "We had a problem paying you #{@distribution.real_distribution_display} for your TestDrive lesson with #{@student.first_name}" + elsif @distribution.is_normal? + @subject = "We had a problem paying you #{@distribution.real_distribution_display} for your lesson with #{@student.first_name}" + elsif @distribution.is_monthly? + @subject = "We had a problem paying you #{@distribution.real_distribution_display} for your #{@distribution.month_name} lessons with #{@student.first_name}" + else + @subject = "We had a problem paying you #{@distribution.real_distribution_display} for your lesson with #{@student.first_name}" end end - def updating_email(user) - @user = user + unique_args = {:type => "teacher_distribution_fail"} - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - sendgrid_unique_args :type => "updating_email" - mail(:to => user.update_email, :subject => "JamKazam Email Change Confirmation") do |format| - format.text - format.html - end + sendgrid_recipients(email) + sendgrid_substitute('@USERID', @distribution.target.lesson_booking.school_over_teacher_ids) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html + end + end + + # always goes to the school owner + def school_distribution_done(teacher_payment) + @school = teacher_payment.school + @teacher_payment = teacher_payment + @distribution = teacher_payment.teacher_distribution + @teacher = teacher_payment.teacher + @payable_teacher = @school.owner + @name = @payable_teacher.first_name || 'Anonymous' + @student = @distribution.student + email = @payable_teacher.email + + if @distribution.is_test_drive? + @subject = "#{@teacher.name} has earned #{@distribution.real_distribution_display} for TestDrive lesson with #{@student.name}" + elsif @distribution.is_normal? + @subject = "#{@teacher.name} has earned #{@distribution.real_distribution_display} for a lesson with #{@student.name}" + elsif @distribution.is_monthly? + @subject = "#{@teacher.name} has earned #{@distribution.real_distribution_display} for #{@distribution.month_name} lessons with #{@student.name}" + else + @subject = "#{@teacher.name} has earned #{@distribution.real_distribution_display} for a lesson with #{@student.name}" end - def updated_email(user) - @user = user + unique_args = {:type => "school_distribution_done"} - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - sendgrid_unique_args :type => "updated_email" - mail(:to => user.email, :subject => "JamKazam Email Changed") do |format| - format.text - format.html - end + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@payable_teacher.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html end + end - def new_musicians(user, new_musicians, host='www.jamkazam.com') - @user, @new_musicians, @host = user, new_musicians, host - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) - sendgrid_unique_args :type => "new_musicians" - mail(:to => user.email, :subject => EmailBatchNewMusician.subject) do |format| - format.text - format.html - end + def monthly_recurring_done(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + + email = @student.email + subject = "Your JamClass lesson today with #{@teacher.first_name}" + unique_args = {:type => "monthly_recurring_done"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - #################################### NOTIFICATION EMAILS #################################### - def friend_request(user, msg, friend_request_id) - return if !user.subscribe_email + def monthly_recurring_no_bill(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session - email = user.email - subject = "You have a new friend request on JamKazam" - unique_args = {:type => "friend_request"} + email = @student.email + subject = "Your lesson with #{@teacher.name} will not be billed" + unique_args = {:type => "student_lesson_normal_done"} - @url = Nav.accept_friend_request_dialog(friend_request_id) - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def friend_request_accepted(user, msg) - return if !user.subscribe_email + def student_lesson_booking_declined(lesson_booking, message) + @lesson_booking = lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @message = message + @lesson_session = lesson_booking.next_lesson + @session_name = @lesson_session.music_session.name + @session_description = @lesson_session.music_session.description + @session_date = @lesson_session.slot.pretty_scheduled_start(true) + email = @student.email + @subject = "We're sorry your lesson request has been declined" + unique_args = {:type => "student_lesson_booking_declined"} - email = user.email - subject = "You have a new friend on JamKazam" - unique_args = {:type => "friend_request_accepted"} + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def new_user_follower(user, msg) - return if !user.subscribe_email + def student_lesson_booking_canceled(lesson_booking, message) + @lesson_booking = lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @message = message + @lesson_session = lesson_booking.next_lesson + @session_name = @lesson_session.music_session.name + @session_description = @lesson_session.music_session.description + @session_date = @lesson_session.slot.pretty_scheduled_start(true) + email = @student.email + @subject = "Your lesson has been canceled" + unique_args = {:type => "student_lesson_booking_canceled"} - email = user.email - subject = "You have a new follower on JamKazam" - unique_args = {:type => "new_user_follower"} + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def new_band_follower(user, msg) - return if !user.subscribe_email + def teacher_lesson_booking_canceled(lesson_booking, message) + @lesson_booking = lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @message = message + @lesson_session = lesson_booking.next_lesson + @session_name = @lesson_session.music_session.name + @session_description = @lesson_session.music_session.description + @session_date = @lesson_session.slot.pretty_scheduled_start(true) - email = user.email - subject = "Your band has a new follower on JamKazam" - unique_args = {:type => "new_band_follower"} + email = @lesson_booking.school_and_teacher + @subject = "Your lesson has been canceled" + unique_args = {:type => "teacher_lesson_booking_canceled"} - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients(email) + sendgrid_substitute('@USERID', @lesson_booking.school_and_teacher_ids) - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def session_invitation(user, msg) - return if !user.subscribe_email + def student_lesson_canceled(lesson_session, message) + @lesson_booking = lesson_booking = lesson_session.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @message = message + @lesson_session = lesson_booking.next_lesson + @session_name = @lesson_session.music_session.name + @session_description = @lesson_session.music_session.description + @session_date = @lesson_session.slot.pretty_scheduled_start(true) + email = @student.email + @subject = "Your lesson has been canceled" + unique_args = {:type => "student_lesson_canceled"} - email = user.email - subject = "You have been invited to a session on JamKazam" - unique_args = {:type => "session_invitation"} + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def musician_session_join(user, msg, session_id) - return if !user.subscribe_email + def teacher_lesson_canceled(lesson_session, message) + @lesson_booking = lesson_booking = lesson_session.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @message = message + @lesson_session = lesson_booking.next_lesson + @session_name = @lesson_session.music_session.name + @session_description = @lesson_session.music_session.description + @session_date = @lesson_session.slot.pretty_scheduled_start(true) - email = user.email - subject = "Someone you know is in a session on JamKazam" - unique_args = {:type => "musician_session_join"} - @body = msg - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + email = @lesson_booking.school_and_teacher + @subject = "Your lesson has been canceled" + unique_args = {:type => "teacher_lesson_canceled"} - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients(email) + sendgrid_substitute('@USERID', @lesson_booking.school_and_teacher_ids) - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def scheduled_session_invitation(user, msg, session) - return if !user.subscribe_email + def invite_school_teacher(school_invitation) + @school_invitation = school_invitation + @school = school_invitation.school - email = user.email - subject = "Session Invitation" - unique_args = {:type => "scheduled_session_invitation"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + email = school_invitation.email + @subject = "#{@school.owner.name} has sent you an invitation to join #{@school.name} on JamKazam" + unique_args = {:type => "invite_school_teacher"} - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + @suppress_user_has_account_footer = true + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html end + end - def scheduled_session_rsvp(user, msg, session) - return if !user.subscribe_email + def invite_school_student(school_invitation) + @school_invitation = school_invitation + @school = school_invitation.school - email = user.email - subject = "Session RSVP" - unique_args = {:type => "scheduled_session_rsvp"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + email = school_invitation.email + @subject = "#{@school.name} has sent you an invitation to join JamKazam for lessons" + unique_args = {:type => "invite_school_student"} - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + @suppress_user_has_account_footer = true + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html end + end - def scheduled_session_rsvp_approved(user, msg, session) - return if !user.subscribe_email + def lesson_chat(chat_msg) + @target = chat_msg.target_user + @sender = chat_msg.user + @message = chat_msg.message + @lesson_session = chat_msg.lesson_session + @session_name = @lesson_session.music_session.name + @session_description = @lesson_session.music_session.description + @session_date = @lesson_session.slot.pretty_scheduled_start(true) - email = user.email - subject = "Session RSVP Approved" - unique_args = {:type => "scheduled_session_rsvp_approved"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + email = @lesson_session.school_over_teacher + @subject = "#{@sender.name} has sent you a message about a lesson" + unique_args = {:type => "lesson_chat"} - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients(email) + sendgrid_substitute('@USERID', @lesson_session.school_over_teacher_ids) - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def scheduled_session_rsvp_cancelled(user, msg, session) - return if !user.subscribe_email + # please respond to outstanding counter! + def student_counter_reminder(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_url = lesson_session.web_url + @lesson_session = lesson_session - email = user.email - subject = "Session RSVP Cancelled" - unique_args = {:type => "scheduled_session_rsvp_cancelled"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + email = @student.email + @subject = "Instructor's time proposal is still awaiting your response" + unique_args = {:type => "student_counter_reminder"} - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def scheduled_session_rsvp_cancelled_org(user, msg, session) - return if !user.subscribe_email + # please respond to outstanding counter! + def teacher_counter_reminder(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + email = lesson_session.school_over_teacher + @subject = "Student #{@student.name}'s time proposal is still awaiting your response" + unique_args = {:type => "teacher_counter_reminder"} - email = user.email - subject = "Your Session RSVP Cancelled" - unique_args = {:type => "scheduled_session_rsvp_cancelled_org"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_recipients(email) + sendgrid_substitute('@USERID', lesson_session.school_over_teacher_ids) - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def scheduled_session_cancelled(user, msg, session) - return if !user.subscribe_email - email = user.email - subject = "Session Cancelled" - unique_args = {:type => "scheduled_session_cancelled"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + def lesson_starting_soon_teacher(lesson_session) + @lesson_booking = lesson_booking = lesson_session.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @lesson_session = lesson_booking.next_lesson + @session_name = @lesson_session.music_session.name + @session_description = @lesson_session.music_session.description + @session_date = @lesson_session.slot.pretty_scheduled_start(true) - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + email = @teacher.email + @subject = "Your lesson with #{@student.first_name} on JamKazam is starting soon" + unique_args = {:type => "send_starting_notice_teacher"} - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def scheduled_session_rescheduled(user, msg, session) - return if !user.subscribe_email + def lesson_starting_soon_student(lesson_session) + @lesson_booking = lesson_booking = lesson_session.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @message = message + @lesson_session = lesson_booking.next_lesson + @session_name = @lesson_session.music_session.name + @session_description = @lesson_session.music_session.description + @session_date = @lesson_session.slot.pretty_scheduled_start(true) - email = user.email - subject = "Session Rescheduled" - unique_args = {:type => "scheduled_session_rescheduled"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + email = @student.email + @subject = "Your lesson with #{@teacher.first_name} on JamKazam is starting soon" + unique_args = {:type => "send_starting_notice_student"} - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def scheduled_session_reminder_upcoming(user, session) - subject = "Your JamKazam session starts in 1 hour!" - unique_args = {:type => "scheduled_session_reminder_upcoming"} - send_scheduled_session_reminder(user, session, subject, unique_args) + def lesson_attachment(sender, target, lesson_session, attachment) + @sender = sender + @target = target + @lesson_session = lesson_session + @attachment = attachment + + + email = target.email + @subject = "An attachment has been added to your lesson by #{sender.name}" + unique_args = {:type => "lesson_attachment"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@target.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end - def scheduled_session_reminder_day(user, session) - subject = "JamKazam Session Reminder" - unique_args = {:type => "scheduled_session_reminder_day"} - send_scheduled_session_reminder(user, session, subject, unique_args) - end - - def send_scheduled_session_reminder(user, session, subject, unique_args) - return if !user.subscribe_email - - email = user.email - @user = user - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def scheduled_session_comment(target_user, sender, msg, comment, session) - return if !target_user.subscribe_email - - email = target_user.email - subject = "New Session Comment" - unique_args = {:type => "scheduled_session_comment"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @comment = comment - @sender = sender - @suppress_user_has_account_footer = true - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [target_user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html { render :layout => "from_user_mailer" } - end - end - - def scheduled_session_daily(receiver, sessions_and_latency) - sendgrid_category "Notification" - sendgrid_unique_args :type => "scheduled_session_daily" - - sendgrid_recipients([receiver.email]) - sendgrid_substitute('@USERID', [receiver.id]) - - @user = receiver - @sessions_and_latency = sessions_and_latency - - @title = 'New Scheduled Sessions Matched to You' - mail(:to => receiver.email, - :subject => EmailBatchScheduledSessions.subject) do |format| - format.text - format.html - end - end - - def band_session_join(user, msg, session_id) - return if !user.subscribe_email - - email = user.email - subject = "A band that you follow has joined a session" - unique_args = {:type => "band_session_join"} - - @body = msg - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def musician_recording_saved(user, msg) - return if !user.subscribe_email - - email = user.email - subject = "A musician has saved a new recording on JamKazam" - unique_args = {:type => "musician_recording_saved"} - - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def band_recording_saved(user, msg) - return if !user.subscribe_email - - email = user.email - subject = "A band has saved a new recording on JamKazam" - unique_args = {:type => "band_recording_saved"} - - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def band_invitation(user, msg) - return if !user.subscribe_email - - email = user.email - subject = "You have been invited to join a band on JamKazam" - unique_args = {:type => "band_invitation"} - - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def band_invitation_accepted(user, msg) - return if !user.subscribe_email - - email = user.email - subject = "Your band invitation was accepted" - unique_args = {:type => "band_invitation_accepted"} - - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - - def text_message(user, sender_id, sender_name, sender_photo_url, message) - return if !user.subscribe_email - - email = user.email - subject = "Message from #{sender_name}" - unique_args = {:type => "text_message"} - - @note = message - @url = Nav.home(dialog: 'text-message', dialog_opts: {d1: sender_id}) - @sender_id = sender_id - @sender_name = sender_name - @sender_photo_url = sender_photo_url - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html { render :layout => "from_user_mailer" } - end - end - - # def send_notification(email, subject, msg, unique_args) - # @body = msg - # sendgrid_category "Notification" - # sendgrid_unique_args :type => unique_args[:type] - # mail(:bcc => email, :subject => subject) do |format| - # format.text - # format.html - # end - # end - ############################################################################################# - end end +end diff --git a/ruby/lib/jam_ruby/app/uploaders/artifact_uploader.rb b/ruby/lib/jam_ruby/app/uploaders/artifact_uploader.rb index 652e877c2..7b2bbab6e 100644 --- a/ruby/lib/jam_ruby/app/uploaders/artifact_uploader.rb +++ b/ruby/lib/jam_ruby/app/uploaders/artifact_uploader.rb @@ -52,7 +52,7 @@ class ArtifactUploader < CarrierWave::Uploader::Base # Add a white list of extensions which are allowed to be uploaded. # For images you might use something like this: def extension_white_list - %w(exe msi dmg) + %w(exe msi dmg zip) end # Override the filename of the uploaded files: diff --git a/ruby/lib/jam_ruby/app/uploaders/music_notation_uploader.rb b/ruby/lib/jam_ruby/app/uploaders/music_notation_uploader.rb index 3f2f49e4f..d098e76cc 100644 --- a/ruby/lib/jam_ruby/app/uploaders/music_notation_uploader.rb +++ b/ruby/lib/jam_ruby/app/uploaders/music_notation_uploader.rb @@ -20,6 +20,6 @@ class MusicNotationUploader < CarrierWave::Uploader::Base end def extension_white_list - %w(pdf png jpg jpeg gif xml mxl txt) + %w(pdf png jpg jpeg gif xml mxl txt wav flac ogg aiff aifc au) end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.html.erb index 6f760961e..358f0f2b1 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.html.erb @@ -1,5 +1,9 @@ -<% provide(:title, 'Confirm Email') %> +<% provide(:title, 'Welcome to JamKazam!') %> -

Welcome to JamKazam, <%= @user.first_name %>!

+

We’re delighted you have joined our community of <%= APP_CONFIG.musician_count %> musicians. We’d like to send you an orientation email with information and resource links that will help you get the most out of JamKazam. Please click here to confirm this email has reached you successfully and we will then send the orientation email.

-

To confirm this email address, please go to the signup confirmation page.

+

If you have received this email but aren’t familiar with JamKazam or JamTracks, then someone has registered at our website using your email address, and you can just ignore and delete this email.

+ +

Best Regards,
+ Team JamKazam +

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.text.erb index d412a7b92..129da288d 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.text.erb @@ -1,3 +1,8 @@ -Welcome to JamKazam, <%= @user.first_name %>! +Welcome to JamKazam! -To confirm this email address, please go to the signup confirmation page at: <%= @signup_confirm_url %>. \ No newline at end of file +We’re delighted you have joined our community of <%= APP_CONFIG.musician_count %> musicians. We’d like to send you an orientation email with information and resource links that will help you get the most out of JamKazam. Please click <%= @signup_confirm_url %> to confirm this email has reached you successfully and we will then send the orientation email. + +If you have received this email but aren’t familiar with JamKazam or JamTracks, then someone has registered at our website using your email address, and you can just ignore and delete this email. + +Best Regards, +Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.html.erb new file mode 100644 index 000000000..164315809 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.html.erb @@ -0,0 +1,16 @@ +<% provide(:title, @subject) %> + +Hello <%= @school_invitation.first_name %> - +

<%= @school.owner.first_name %> is using JamKazam to deliver online music lessons, and has sent you this invitation so that you can + register to take online music lessons with <%= @school.name %>. To accept this invitation, please click the SIGN UP NOW + button below, and follow the instructions on the web page to which you are taken. Thanks, and on behalf of + <%= @school.name %>, welcome to JamKazam!

+
+

+ SIGN + UP NOW +

+
+
+Best Regards,
+Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.text.erb new file mode 100644 index 000000000..1b1ea765b --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.text.erb @@ -0,0 +1,12 @@ +<% provide(:title, @subject) %> + +Hello <%= @school_invitation.first_name %> - +<%= @school.owner.first_name %> is using JamKazam to deliver online music lessons, and has sent you this invitation so that you can +register to take online music lessons with <%= @school.name %>. To accept this invitation, please click the link +below, and follow the instructions on the web page to which you are taken. Thanks, and on behalf of +<%= @school.name %>, welcome to JamKazam! + +<%= @school_invitation.generate_signup_url %> + +Best Regards,
+Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.html.erb new file mode 100644 index 000000000..6e59f21d8 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.html.erb @@ -0,0 +1,18 @@ +<% provide(:title, @subject) %> + +Hello <%= @school_invitation.first_name %> - +
+

+ <%= @school.owner.first_name %> has set up <%= @school.name %> on JamKazam, enabling you to deliver online music + lessons in an amazing new way that really works. To accept this invitation, please click the SIGN UP NOW button below, + and follow the instructions on the web page to which you are taken. Thanks, and welcome to JamKazam!

+
+

+ SIGN + UP NOW +

+ +
+
+Best Regards,
+Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.text.erb new file mode 100644 index 000000000..c31475ea8 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.text.erb @@ -0,0 +1,11 @@ +<% provide(:title, @subject) %> + +Hello <%= @school_invitation.first_name %> - +<%= @school.owner.first_name %> has set up <%= @school.name %> on JamKazam, enabling you to deliver online music +lessons in an amazing new way that really works. To accept this invitation, please click the link below, +and follow the instructions on the web page to which you are taken. Thanks, and welcome to JamKazam! + +<%= @school_invitation.generate_signup_url %> + +Best Regards, +Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_attachment.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_attachment.html.erb new file mode 100644 index 000000000..fc49d2163 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_attachment.html.erb @@ -0,0 +1,20 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @sender.resolved_photo_url) %> + +<% content_for :note do %> +

+ <% if @attachment.is_a?(JamRuby::MusicNotation) %> + <% if @attachment.is_notation? %> + A music notation has been added to your lesson. You can download "><%= @attachment.file_name %> directly or at any time in the message window for this lesson. + <% else %> + A audio file has been added to your lesson. You can download "><%= @attachment.file_name %> directly or at any time in the message window for this lesson. + <% end %> + <% else %> + A recording named "<%= @attachment.name %>" has been added to your lesson. It can be viewed ">here or found within the message window for this lesson. + <% end %> +

+ VIEW LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_attachment.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_attachment.text.erb new file mode 100644 index 000000000..8756d460c --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_attachment.text.erb @@ -0,0 +1,12 @@ +<% if @attachment.is_a?(JamRuby::MusicNotation) %> +<% if @attachment.is_notation? %> +A music notation has been added to your lesson. You can download (<%= APP_CONFIG.external_root_url + "/api/music_notations/#{@attachment.id}" %>">)<%= @attachment.file_name %> directly or at any time in the message window for this lesson. +<% else %> +A audio file has been added to your lesson. You can download (<%= APP_CONFIG.external_root_url + "/api/music_notations/#{@attachment.id}" %>">)<%= @attachment.file_name %> directly or at any time in the message window for this lesson. +<% end %> +<% else %> +A recording named "<%= @attachment.name %>" has been added to your lesson. It can be viewed (<%= APP_CONFIG.external_root_url + "/recordings/#{@attachment.id}" %>">) here or found within the message window for this lesson. +<% end %> +VIEW LESSON DETAILS (<%= @lesson_session.web_url %>) + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.html.erb new file mode 100644 index 000000000..4b11f3cc0 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.html.erb @@ -0,0 +1,18 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @sender.resolved_photo_url) %> + +<% content_for :note do %> +

+ + <% if @message.present? %> +

<%= @sender.name %> says: +
<%= @message %> +
+ <% end %> +

Click the button below to view the entire lesson conversation.

+

+ VIEW LESSON CONVERSATION +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.text.erb new file mode 100644 index 000000000..43b683250 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.text.erb @@ -0,0 +1,7 @@ +<%= @subject %> + +<%= @sender.name %> says: + +<%= @message %> + +To see the full lesson conversation, click here: <%= @lesson_session.chat_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.html.erb new file mode 100644 index 000000000..d4744ff94 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.html.erb @@ -0,0 +1,12 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Your lesson with <%= @teacher.name %> is scheduled to begin on JamKazam in less than 30 minutes. +

+ JAMCLASS HOME +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.text.erb new file mode 100644 index 000000000..f880fcb55 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.text.erb @@ -0,0 +1,4 @@ +Your lesson with <%= @teacher.name %> is scheduled to begin on JamKazam in less than 30 minutes. + +JAMCLASS HOME (<%= @lesson_session.home_url %> + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.html.erb new file mode 100644 index 000000000..96ecf3c06 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.html.erb @@ -0,0 +1,12 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

+ Your lesson with <%= @student.name %> is scheduled to begin on JamKazam in less than 30 minutes. +

+ JAMCLASS HOME +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.text.erb new file mode 100644 index 000000000..c8f460e25 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.text.erb @@ -0,0 +1,3 @@ +Your lesson with <%= @student.name %> is scheduled to begin on JamKazam in less than 30 minutes. +JAMCLASS HOME (<%= @lesson_session.home_url %> + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.html.erb new file mode 100644 index 000000000..bc205e230 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.html.erb @@ -0,0 +1,28 @@ +<% provide(:title, "Your JamClass lesson today with #{@teacher.first_name}") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

+ We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. +

+ +

+ <% if !@student.has_rated_teacher(@teacher) %> + If you haven't already done so, please rate your teacher now to help other students in the community find the best + instructors. + <% end %> + + If you had technical problems during your lesson, or have questions, or would like to make suggestions + on how to improve JamClass, please email us at support@jamkazam.com. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.text.erb new file mode 100644 index 000000000..96bf19889 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.text.erb @@ -0,0 +1,28 @@ +<% provide(:title, "Your JamClass lesson today with #{@teacher.first_name}") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

+ We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. As just a reminder, you already paid for this lesson in advance. +

+ +

+ <% if !@student.has_rated_teacher(@teacher) %> + If you haven't already done so, please rate your teacher now to help other students in the community find the best + instructors. + <% end %> + + If you had technical problems during your lesson, or have questions, or would like to make suggestions + on how to improve JamClass, please email us at support@jamkazam.com. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.html.erb new file mode 100644 index 000000000..3dc58d8ec --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, "Your lesson with #{@teacher.name} will not be billed") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

You will not be billed for today's session with <%= @teacher.name %>. However, you already paid for the lesson in advance, so next month's bill will be lower than usual. +
+
+ Click the button below to see more information about this session. +

+

+ VIEW + LESSON DETAILS +

+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.text.erb new file mode 100644 index 000000000..f21f164b5 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.text.erb @@ -0,0 +1,5 @@ +Hello <%= @student.name %>, + +You will not be billed for today's session with <%= @teacher.name %>. However, you already paid for the lesson in advance, so next month's bill will be lower than usual. + +To see this lesson, click here: <%= @lesson_session.web_url %> 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 be9aa27fb..65bf1295f 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 @@ -1,5 +1,7 @@ <% provide(:title, 'New Musicians You Should Check Out') %> -Hi <%= @user.first_name %>, +<% if !@user.anonymous? %> +

Hi <%= @user.first_name %>,

+<% end %>

The following new musicians have joined JamKazam within the last week, and have Internet connections with low enough latency to you that you can have a good online session together. We'd suggest that you look through the new musicians listed below to see if any match your musical interests, and if so, click through to their profile page on the JamKazam website to send them a message or a request to connect as a JamKazam friend:

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 05fbbd268..ee100dbff 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 @@ -1,7 +1,8 @@ New Musicians You Should Check Out +<% if !@user.anonymous? %> Hi <%= @user.first_name %>, - +<% end %> The following new musicians have joined JamKazam within the last week, and have Internet connections with low enough latency to you that you can have a good online session together. We'd suggest that you look through the new musicians listed below to see if any match your musical interests, and if so, click through to their profile page on the JamKazam website to send them a message or a request to connect as a JamKazam friend: <% @new_musicians.each do |user| %> 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 2fdc11256..769b5bcda 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 @@ -1,7 +1,8 @@ <% provide(:title, @title) %> -

Hello <%= @user.first_name %> -- -

+<% if !@user.anonymous? %> +

Hi <%= @user.first_name %>,

+<% end %>

The following new sessions have been posted within the last 24 hours, and you have good or acceptable latency to the organizer of each session below. If a session looks interesting, click the Details link to see the session page. You can RSVP to a session from the session page, and you'll be notified if/when the session organizer approves your RSVP.

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 282c6dd90..3d1e15346 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 @@ -1,7 +1,8 @@ <% provide(:title, @title) %> -Hello <%= @user.first_name %> -- - +<% if !@user.anonymous? %> +Hi <%= @user.first_name %>, +<% end %> The following new sessions have been posted within the last 24 hours, and you have good or acceptable latency to the organizer of each session below. If a session looks interesting, click the Details link to see the session page. You can RSVP to a session from the session page, and you'll be notified if/when the session organizer approves your RSVP. GENRE | NAME | DESCRIPTION | LATENCY diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb index b72d3c133..cf4816bf1 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb @@ -1,18 +1,20 @@ <% provide(:title, 'JamKazam Session Reminder') %> -
+<% if !@user.anonymous? %> +

Hi <%= @user.first_name %>, -

+

+<% end %>
-
+

This is a reminder that your JamKazam session <%= @session_name %> is scheduled for tomorrow. We hope you have fun! -

+


-
+

Best Regards,
Team JamKazam -

\ No newline at end of file +

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb index c3f0576bf..333acdb74 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb @@ -1,5 +1,6 @@ +<% if !@user.anonymous? %> Hi <%= @user.first_name %>, - +<% end %> This is a reminder that your JamKazam session <%=@session_name%> is scheduled for tomorrow. We hope you have fun! Best Regards, diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb index 4fbc59ace..119d57b16 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb @@ -1,17 +1,20 @@ <% provide(:title, 'Your JamKazam session starts in 1 hour!') %> -
+<% if !@user.anonymous? %> +

Hi <%= @user.first_name %>, -

+

+<% end %> +
-
+

This is a reminder that your JamKazam session <%= @session_name %> starts in 1 hour. We hope you have fun! -

+


-
+

Best Regards,
Team JamKazam -

+

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb index 70726a9e6..d4719bd4c 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb @@ -1,4 +1,6 @@ +<% if !@user.anonymous? %> Hi <%= @user.first_name %>, +<% end %> This is a reminder that your JamKazam session <%=@session_name%> diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_distribution_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_distribution_done.html.erb new file mode 100644 index 000000000..37f9d2780 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_distribution_done.html.erb @@ -0,0 +1,20 @@ +<% provide(:title, @subject) %> + +

+ Hello <%= @name %>, +

+ + <% if @distribution.is_test_drive? %> +

We have processed a payment to you via your Stripe account for $<%= @distribution.real_distribution_display %> for this lesson.

+ <% elsif @distribution.is_normal? %> +

We have processed a payment to you via your Stripe account for $<%= @distribution.real_distribution_display %> for this lesson.

+ <% elsif @distribution.is_monthly? %> +

We have processed a payment to you via your Stripe account for $<%= @distribution.real_distribution_display %> for <%= @distribution.month_name %> lessons.

+ <% else %> + Unknown payment type. + <% end %> +
+
+ +Best Regards,
+JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_distribution_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_distribution_done.text.erb new file mode 100644 index 000000000..fcf56ab98 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_distribution_done.text.erb @@ -0,0 +1,16 @@ +<% provide(:title, @subject) %> + +Hello <%= @name %>, + +<% if @distribution.is_test_drive? %> +We have processed a payment to you via your Stripe account for $<%= @distribution.real_distribution_display %> for this lesson. +<% elsif @distribution.is_normal? %> +We have processed a payment to you via your Stripe account for $<%= @distribution.real_distribution_display %> for this lesson. +<% elsif @distribution.is_monthly? %> +We have processed a payment to you via your Stripe account for $<%= @distribution.real_distribution_display %> for <%= @distribution.month_name %> lessons. +<% else %> +Unknown payment type. +<% end %> + +Best Regards, +JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_owner_welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_owner_welcome_message.html.erb new file mode 100644 index 000000000..7df3b4c61 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_owner_welcome_message.html.erb @@ -0,0 +1,39 @@ +<% provide(:title, @subject) %> + + +<% if !@user.anonymous? %> +

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +

+<% end %> + +

+ Thank you for expressing an interest in exploring our music school partner program! A member of our staff will reach out to you shortly to chat with you and answer any/all questions you may have about our partner program, our technologies, and how we can help you continue to build your music school business. +

+ +

+ We'd also like to provide links to some help articles that explain how many things work, and will likely answer many of your questions in a well-organized manner: +

+ + +

Guide for Music School Owners
+ These help articles explain things from the perspective of the school owner - e.g. how you can schedule and book lessons from our marketplace with your teachers, how billing and payments are handled, and so on. +

+ +

Guide for Music Lesson Teachers
+ These help articles explain how teachers use the features of the platform outside of the online lesson sessions. +

+ +

Key Features To Use In Online Sessions
+ These help articles explain the key features instructors can use in online sessions to teach effectively. +

+ +

+ Gear Requirements
+ This help article explains the requirements for your computer, audio and video gear, and Internet service. +

+ +

+ Thanks again for connecting with us, and we look forward to speaking with you soon! +

+

Best Regards,
+ Team JamKazam

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_owner_welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_owner_welcome_message.text.erb new file mode 100644 index 000000000..771a82b03 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_owner_welcome_message.text.erb @@ -0,0 +1,24 @@ +<% if !@user.anonymous? %> +Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> +<% end %> + +Thank you for expressing an interest in exploring our music school partner program! A member of our staff will reach out to you shortly to chat with you and answer any/all questions you may have about our partner program, our technologies, and how we can help you continue to build your music school business. + +We'd also like to provide links to some help articles that explain how many things work, and will likely answer many of your questions in a well-organized manner: + +-- Guide for Music School Owners (https://jamkazam.desk.com/customer/en/portal/topics/935633-jamclass-online-music-lessons---for-music-schools/articles) +These help articles explain things from the perspective of the school owner - e.g. how you can schedule and book lessons from our marketplace with your teachers, how billing and payments are handled, and so on. + +-- Guide for Music Lesson Teachers (https://jamkazam.desk.com/customer/en/portal/topics/926076-jamclass-online-music-lessons---for-teachers/articles) +These help articles explain how teachers use the features of the platform outside of the online lesson sessions. + +-- Key Features To Use In Online Sessions (https://jamkazam.desk.com/customer/en/portal/topics/673198-key-features-to-use-in-online-sessions/articles) +These help articles explain the key features instructors can use in online sessions to teach effectively. + +-- Gear Requirements (https://jamkazam.desk.com/customer/en/portal/articles/1288274-computer-internet-audio-and-video-requirements) +This help article explains the requirements for your computer, audio and video gear, and Internet service. + +Thanks again for connecting with us, and we look forward to speaking with you soon! + +Best Regards, +Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_counter_reminder.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_counter_reminder.html.erb new file mode 100644 index 000000000..f437bd8ad --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_counter_reminder.html.erb @@ -0,0 +1,17 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ <%= @teacher.name %> proposed a different time 24 hours ago. +
+
+ Please click the button below to respond. +

+

+ VIEW + LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_counter_reminder.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_counter_reminder.text.erb new file mode 100644 index 000000000..e05617102 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_counter_reminder.text.erb @@ -0,0 +1,3 @@ +<%= @teacher.name %> has proposed a different time 24 hours ago. Please respond. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.html.erb new file mode 100644 index 000000000..a5b2c826a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.html.erb @@ -0,0 +1,24 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_session.teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ <% if @slot.is_teacher_approved? %> + This teacher has accepted your lesson request! + <% else %> + You have confirmed a lesson request. + <% end %> + + <% if @message.present? %> +

<%= @sender.name %> says: +
<%= @message %> +
+ <% end %> + +

We strongly suggest adding this to your calendar so you don't forget it, or you'll end up paying for a lesson you don't get.

+

+ VIEW LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.text.erb new file mode 100644 index 000000000..c7506b0ad --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.text.erb @@ -0,0 +1,3 @@ +<%= @subject %> + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.html.erb new file mode 100644 index 000000000..bc819bb5e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.html.erb @@ -0,0 +1,29 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_booking.canceler.resolved_photo_url) %> + +<% content_for :note do %> +

+ <% if @lesson_booking.recurring %> + + All lessons that were scheduled for <%= @lesson_booking.dayWeekDesc %> with <%= @teacher.name %> have been canceled. + + <% else %> + Your lesson with <%= @teacher.name %> has been canceled.

+ + Session Name: <%= @session_name %>
+ Session Description: <%= @session_description %>
+ <%= @session_date %> + <% end %> + + <% if @message.present? %> +

<%= @lesson_booking.canceler.name %> says: +
<%= @message %> +
+ <% end %> +

Click the button below to view more info about the canceled session.

+

+ VIEW LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.text.erb new file mode 100644 index 000000000..c7506b0ad --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.text.erb @@ -0,0 +1,3 @@ +<%= @subject %> + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.html.erb new file mode 100644 index 000000000..410621edb --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.html.erb @@ -0,0 +1,20 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ + This teacher has declined your lesson request. + + <% if @message.present? %> +

<%= @teacher.name %> says: +
<%= @message %> +
+ <% end %> +

Click the button below to view the teacher's response.

+

+ VIEW RESPONSE +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.text.erb new file mode 100644 index 000000000..c7506b0ad --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.text.erb @@ -0,0 +1,3 @@ +<%= @subject %> + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.html.erb new file mode 100644 index 000000000..484bccb78 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_session.canceler.resolved_photo_url) %> + +<% content_for :note do %> +

+ Your lesson with <%= @teacher.name %> has been canceled.

+ + Session Name: <%= @session_name %>
+ Session Description: <%= @session_description %>
+ <%= @session_date %> + + <% if @message.present? %> +

<%= @lesson_session.canceler.name %> says: +
<%= @message %> +
+ <% end %> +

Click the button below to view more info about the canceled session.

+

+ VIEW RESPONSE +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.text.erb new file mode 100644 index 000000000..c7506b0ad --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.text.erb @@ -0,0 +1,3 @@ +<%= @subject %> + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.html.erb new file mode 100644 index 000000000..38ec8a404 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.html.erb @@ -0,0 +1,17 @@ +<% provide(:title, "#{@teacher.name} has proposed a different time for your lesson") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ <%= @teacher.name %> has proposed a different time for your lesson request. +
+
+ Click the button below to get more information and respond. +

+

+ VIEW + LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.text.erb new file mode 100644 index 000000000..67972c906 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.text.erb @@ -0,0 +1,3 @@ +<%= @teacher.name %> has proposed a different time for your lesson request. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.html.erb new file mode 100644 index 000000000..d795f64b4 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.html.erb @@ -0,0 +1,28 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

+ You have been billed $<%= @lesson_package_purchase.amount_charged %> for this month's lessons with <%= @teacher.name %>. +

+ +

+ <% if !@student.has_rated_teacher(@teacher) %> + If you haven't already done so, please rate your teacher now to help other students in the community find the best + instructors. + <% end %> + + If you had technical problems during your lesson, or have questions, or would like to make suggestions + on how to improve JamClass, please email us at support@jamkazam.com. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.text.erb new file mode 100644 index 000000000..7328cf9a7 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.text.erb @@ -0,0 +1,14 @@ +Hello <%= @student.name %>, + +You have been billed $<%= @lesson_package_purchase.amount_charged %> for this month's lessons with <%= @teacher.name %>. + +<% if !@student.has_rated_teacher(@teacher) %> +If you haven't already done so, please rate your teachernow to help other students in the community find the best instructors. <%= @teacher.ratings_url %> +<% end %> + +If you had technical problems during your lesson, or have questions, or would like to make suggestions +on how to improve JamClass, please email us at support@jamkazam.com. + +Best Regards, +Team JamKazam + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_done.html.erb new file mode 100644 index 000000000..d795f64b4 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_done.html.erb @@ -0,0 +1,28 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

+ You have been billed $<%= @lesson_package_purchase.amount_charged %> for this month's lessons with <%= @teacher.name %>. +

+ +

+ <% if !@student.has_rated_teacher(@teacher) %> + If you haven't already done so, please rate your teacher now to help other students in the community find the best + instructors. + <% end %> + + If you had technical problems during your lesson, or have questions, or would like to make suggestions + on how to improve JamClass, please email us at support@jamkazam.com. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.html.erb new file mode 100644 index 000000000..a6a2cddc7 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.html.erb @@ -0,0 +1,28 @@ +<% provide(:title, "Your JamClass lesson today with #{@teacher.first_name}") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

+ We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. You have been billed $<%= @lesson_session.amount_charged %> for today's lesson. +

+ +

+ <% if !@student.has_rated_teacher(@teacher) %> + If you haven't already done so, please rate your teacher now to help other students in the community find the best + instructors. + <% end %> + + If you had technical problems during your lesson, or have questions, or would like to make suggestions + on how to improve JamClass, please email us at support@jamkazam.com. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.text.erb new file mode 100644 index 000000000..efdccf098 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.text.erb @@ -0,0 +1,16 @@ +Hello <%= @student.name %>, + +We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. You have been billed $<%= @lesson_session.amount_charged %> for today's lesson. + +<% if !@student.has_rated_teacher(@teacher) %> + If you haven't already done so, please rate your teacher now to help other students in the community find the best + instructors. You can rate your teacher here: <%= @teacher.ratings_url %> +<% end %> + +If you had technical problems during your lesson, or have questions, or would like to make suggestions +on how to improve JamClass, please email us at support@jamkazam.com. + +Best Regards, +Team JamKazam + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.html.erb new file mode 100644 index 000000000..aa263319f --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, "Your lesson with #{@teacher.name} will not be billed") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

You will not be billed for today's session with <%= @teacher.name %>. +
+
+ Click the button below to see more information about this session. +

+

+ VIEW + LESSON DETAILS +

+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.text.erb new file mode 100644 index 000000000..31ef00ca0 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.text.erb @@ -0,0 +1,5 @@ +Hello <%= @student.name %>, + +You will not be billed for today's session with <%= @teacher.name %>. + +To see this lesson, click here: <%= @lesson_session.web_url %> diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_request.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_request.html.erb new file mode 100644 index 000000000..4636052b0 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_request.html.erb @@ -0,0 +1,11 @@ +<% provide(:title, "Lesson requested of #{@sender.name}") %> +<% provide(:photo_url, @sender.resolved_photo_url) %> + +<% content_for :note do %> +

You have requested a <%= @lesson_booking.display_type %> lesson.

Click the button below to see your lesson request. You will receive another email when the teacher accepts or rejects the request.

+

+ VIEW LESSON REQUEST +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_request.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_request.text.erb new file mode 100644 index 000000000..9ec7f863e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_request.text.erb @@ -0,0 +1,3 @@ +You have requested a lesson from <%= @sender.name %>. + +To see this lesson request, click here: <%= @lesson_booking.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.html.erb new file mode 100644 index 000000000..d0259e567 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.html.erb @@ -0,0 +1,20 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_session.teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ All lessons with <%= @lesson_session.teacher.name %> have been rescheduled. + + <% if @message.present? %> +

<%= @sender.name %> says: +
<%= @message %> +
+ <% end %> +

Click the button below to get more information and to update your calendar! +

We strongly suggest adding this to your calendar so you don't forget it, or you'll end up paying for a lesson you don't get.

+

+ VIEW LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.text.erb new file mode 100644 index 000000000..6011f7e65 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.text.erb @@ -0,0 +1,3 @@ +All your lessons with <%= @lesson_session.teacher.name%> have been rescheduled. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.html.erb new file mode 100644 index 000000000..f192b7b9c --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.html.erb @@ -0,0 +1,11 @@ +<% provide(:title, @subject) %> + +

<%= @body %>

+ +

+ Session Name: <%= @session_name %>
+ <%= @session_description %>
+ <%= @session_date %> +

+ +

VIEW LESSON DETAILS

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.text.erb new file mode 100644 index 000000000..63679c0d2 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.text.erb @@ -0,0 +1,7 @@ +<%= @body %> + +<%= @session_name %> +<%= @session_description %> +<%= @session_date %> + +See session details at <%= @session_url %>. \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb new file mode 100644 index 000000000..56e8750e5 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb @@ -0,0 +1,30 @@ +<% provide(:title, "You have used #{@student.used_test_drives} of #{@student.total_test_drives} TestDrive lesson credits") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. You have + used <%= @student.used_test_drives %> TestDrive credits, and you have <%= @student.remaining_test_drives %> + remaining TestDrive lesson(s) available. If you haven’t booked your next TestDrive lesson, + click here to search our teachers and get your next + lesson lined up today!

+ +

+ <% if !@student.has_rated_teacher(@teacher) %> + Also, please rate your teacher now for today’s lesson + to help other students in the community find the best instructors. + <% end %> + And if you had technical problems during your lesson, or have questions, or would like to make suggestions on how to + improve JamClass, + please email us at support@jamkazam.com. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb new file mode 100644 index 000000000..34db7e7bc --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb @@ -0,0 +1,14 @@ +You have used <%= @student.used_test_drives %> of <%= @student.total_test_drives %> TestDrive lesson credits. + +<% if @student.has_rated_teacher(@teacher) %> + Also, please rate your teacher at <%= @teacher.ratings_url %> now for today’s lesson to help other students in the community find the best instructors. +<% end %> + +If you clicked with one of your TestDrive instructors, here are links to each teacher’s listing. You can use the +link to your favorite to book single or weekly recurring lessons with the best instructor for you! +<% @student.recent_test_drive_teachers.each do |teacher| %> +<%= teacher.name %>: <%= teacher.teacher_profile_url %> +<% end %> + +And if you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, +please email us at support@jamkazam.com. diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.html.erb new file mode 100644 index 000000000..d99b8494a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.html.erb @@ -0,0 +1,47 @@ +<% provide(:title, "You have used all TestDrive lesson credits") %> + +

+ Hello <%= @student.name %>, +

+ +

+ We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. You have now used all your TestDrive credits. + + <% if !@student.has_rated_teacher(@teacher) %> + Please rate your teacher now for today’s lesson to + help other students in the community find the best instructors. + <% end %> +

+

+ If you clicked with one of your TestDrive instructors, here are links to each teacher’s listing. You can use the + link to your favorite to book single or weekly recurring lessons with the best instructor for you! +

+<% @student.recent_test_drive_teachers.each do |teacher| %> + + + + + + +
+

+ +

+ <%= teacher.name %> +

+
+ +<% end %> + +

+ And if you had technical problems during your lesson, or have questions, or would like to make suggestions on how + to improve JamClass, + please email us at support@jamkazam.com.

+
+ +

+ Best Regards,
Team JamKazam +

+ + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.text.erb new file mode 100644 index 000000000..f80c5c8cb --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.text.erb @@ -0,0 +1,8 @@ +You have used all of your TestDrive lesson credits. + +<% if @student.has_rated_teacher(@teacher) %> + Also, please rate your teacher at <%= @teacher.ratings_url %> now for today’s lesson to help other students in the community find the best instructors. +<% end %> + +And if you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, +please email us at support@jamkazam.com. diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.html.erb new file mode 100644 index 000000000..a10a0bc5d --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, "Your TestDrive with #{@teacher.name} will not be billed") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

You have not used a credit for today's TestDrive with <%= @teacher.name %> because <%= @lesson_session.error_display %>. +
+
+ Click the button below to see more information about this session. +

+

+ VIEW + LESSON DETAILS +

+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.text.erb new file mode 100644 index 000000000..f3d3b2825 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.text.erb @@ -0,0 +1,5 @@ +Hello <%= @student.name %>, + +You have not used a credit for today's TestDrive with <%= @teacher.name %>. + +To see this lesson, click here: <%= @lesson_session.web_url %> diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.html.erb new file mode 100644 index 000000000..2da383332 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.html.erb @@ -0,0 +1,28 @@ +<% provide(:title, "The credit card charge for your lesson today with #{@teacher.first_name} failed") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

+ + <% if @card_declined %> + When we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge was declined. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! + <% elsif @card_expired %> + When we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge failed because the card is expired. To update your credit card information, please use the button below to go to a page where you can change your card info. Thank you! + <% else %> + For some reason, when we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge failed. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! + <% end %> + +

+

+ UPDATE PAYMENT INFO +

+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.text.erb new file mode 100644 index 000000000..b4cfb4628 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.text.erb @@ -0,0 +1,18 @@ +The credit card charge for your lesson today with <%= @teacher.first_name %> failed. +Hello <%= @student.name %>, + +<% if @card_declined %> + When we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge was declined. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! +<% elsif @card_expired %> + When we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge failed because the card is expired. To update your credit card information, please use the button below to go to a page where you can change your card info. Thank you! +<% else %> + For some reason, when we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge failed. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! +<% end %> + + +Update Payment info here: <%= @lesson_session.update_payment_url %> + +Best Regards, +Team JamKazam + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.html.erb new file mode 100644 index 000000000..f4d5f5402 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.html.erb @@ -0,0 +1,32 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

+ <% if @lesson_booking.is_suspended? %> + Your weekly lessons with <%= @teacher.name %> have been suspended because we have tried repeatedly to charge your credit card but have failed. +
+
+ <% end %> + + <% if @card_declined %> + When we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge was declined. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! + <% elsif @card_expired %> + When we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge failed because the card is expired. To update your credit card information, please use the button below to go to a page where you can change your card info. Thank you! + <% else %> + For some reason, when we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge failed. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! + <% end %> +

+

+ UPDATE PAYMENT INFO +

+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.text.erb new file mode 100644 index 000000000..2567d6891 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.text.erb @@ -0,0 +1,23 @@ +Hello <%= @student.name %>, + +<%= @subject %> + +<% if @lesson_booking.is_suspended? %> + Your weekly lessons with <%= @teacher.name %> have been suspended because we have tried repeatedly to charge your credit card but have failed. +<% end %> + +<% if @card_declined %> + When we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge was declined. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! +<% elsif @card_expired %> + When we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge failed because the card is expired. To update your credit card information, please use the button below to go to a page where you can change your card info. Thank you! +<% else %> + For some reason, when we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge failed. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! +<% end %> + + +Update Payment info here: <%= @lesson_booking.update_payment_url %> + +Best Regards, +Team JamKazam + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb new file mode 100644 index 000000000..d2d9a47f2 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb @@ -0,0 +1,52 @@ +<% provide(:title, @subject) %> + + +<% if !@user.anonymous? %> +

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +

+<% end %> + + +

+ Thank you for signing up to take online music lessons using the JamClass service by JamKazam. JamKazam technology was + built from the ground up for playing music live in sync with high quality audio from different locations over the + Internet. Unlike other lesson services, this means we can deliver a massively better online music lesson experience + than voice/chat apps like Skype, etc. Our rapidly growing community of <%= APP_CONFIG.musician_count %> musicians will attest to this. +

+ +

+ To get ready to take JamClass lessons online, here are the things you'll want to do: +

+ +

1. Find a Teacher & Book Lessons
+ + If you haven't done so already, use this link to search our teachers, and click to book a TestDrive with a teacher who looks good for you. When you do this, you'll be given the option to take full 30-minute TestDrive lessons: + +

    +
  • With 4 different teachers for just $12.50 each
  • +
  • With 2 different teachers for just $14.99 each
  • +
  • Or with 1 teacher for just $14.99
  • +
+

+ Pick whichever option you prefer. TestDrive lets you safely and easily try multiple teachers to find the one who is best specifically for you, which is a great way to maximize the benefit from your lessons. And TestDrive lessons are heavily discounted to give you a risk-free way to get started. We'd suggest scheduling your first lesson for about a week in the future to give you plenty of time to get up and running with our free app. +

+

+ +

2. Set Up Your Gear
+ Please review our help articles on gear recommendations + to make sure you have everything you need to get set up properly for best results in your online lessons. + If you have everything you need, then you can follow the instructions on our setup help articles to download and install our free app and set it up with your audio gear and webcam. Please email us at support@jamkazam.com or call us at 1-877-376-8742 any time so that we can help you with these steps. We are very happy to help, and we also strongly suggest that you let one of our staff get into an online session with you to make sure everything is working properly and to make sure you're comfortable with the app and ready for your first lesson. +

+ +

3. Learn About JamClass Features
+ Please review our JamClass user guide for students to familiarize yourself with the features and resources available to you through our JamClass lesson service. This includes how to search for the best teacher for you, how to request/book lessons, how to join your teacher in online lessons, features you can use while in lessons, and much more. +

+ +

+ + Again, welcome to JamKazam and our JamClass online music lesson service, and we look forward to helping you learn and grow as a musician! +

+ + +

Best Regards,
+ Team JamKazam

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.text.erb new file mode 100644 index 000000000..b16eef375 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.text.erb @@ -0,0 +1,57 @@ +<% if !@user.anonymous? %>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --<% end %> + +Thank you for signing up to take online music lessons using the JamClass service by JamKazam. JamKazam technology was +built from the ground up for playing music live in sync with high quality audio from different locations over the +Internet. Unlike other lesson services, this means we can deliver a massively better online music lesson experience +than voice/chat apps like Skype, etc. Our rapidly growing community of <%= APP_CONFIG.musician_count %> musicians will attest to this. + + +To get ready to take JamClass lessons online, here are the things you'll want to do: + +1. Find a Teacher & Book Lessons +If you already know the teacher from whom you want to learn, then you can simply +use this link to search for them (https://www.jamkazam.com/client#/jamclass/searchOptions), and +click the Book Lesson button to get started. But if you're like most of us, you don't know. In this case, we strongly +advise signing up for our unique TestDrive service. + +TestDrive lets you take 4 full lessons (30 minutes each) from 4 different teachers for just $49.99 to find the best +teacher for you. Finding the right teacher is the single most important determinant of success in your lessons. Would +you marry the first person you ever dated? No? Same here. Pick 4 teachers who look great, and then see who you click +with. It's a phenomenal value, and then you can stick with the best teacher for you. +Click this link to sign up now for TestDrive (https://www.jamkazam.com/client#/jamclass/test-drive-selection). +Then you can book 4 TestDrive lessons to get rolling. + +2. Set Up Your Gear +Use this link to a set of +help articles on how to set up your gear (https://jamkazam.desk.com/customer/en/portal/topics/673197-first-time-setup/articles) +to be ready to teach online. After you have signed +up, someone from JamKazam will contact you to schedule a test online session, in which we will make sure your audio +and video gear are working properly in an online session, and to make sure you feel comfortable with the key features +you will be using in sessions with teachers. + +3. Learn About JamClass Features +Use this link to a set of help articles for students on JamClass (https://jamkazam.desk.com/customer/en/portal/topics/926073-jamclass-online-music-lessons---for-students/articles) +to familiarize yourself with the most useful features +for online lessons. This includes how to search for the best teacher for you, how to request/book lessons, how to join +your teacher in online lessons, features you can use while in lessons, and much more. There is very important basic +information, plus some really nifty stuff here, so be sure to look through it at least briefly to see how we can +turbocharge your online lessons! + +4. Play With Other Musicians Online - It's Free! +With JamKazam, you can use the things you're learning in lessons to play with other amateur musicians in online +sessions, free! Or just play for fun. Once you've set up your gear for lessons, you can +create online music sessions (https://jamkazam.desk.com/customer/en/portal/articles/1599977-creating-a-session) +that others can join, or find other musicians' online music sessions (https://jamkazam.desk.com/customer/en/portal/articles/1599978-finding-a-session) +and hop into those to play with others. If you +want to take advantage of this part of the JamKazam platform, we'd advise that you edit your musician profile (https://www.jamkazam.com/client#/account/profile) to make +it easier to connect with other musicians (https://jamkazam.desk.com/customer/en/portal/articles/1707418-connecting-with-other-musicians) in our community to expand your set of musician friends. It's a ton of fun, +so give it a try! + +As you work through these things, if you ever get stuck or have questions, please don't hesitate to reach out for +help. You can email us any time at support@jamkazam.com. We are happy to +help you, and we look forward to helping you +learn and grow as a musician, and expand your musical universe! + +Best Regards, +Team JamKazam + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_counter_reminder.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_counter_reminder.html.erb new file mode 100644 index 000000000..5b5efd606 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_counter_reminder.html.erb @@ -0,0 +1,17 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

+ <%= @student.name %> has proposed a different time 24 hours ago. +
+
+ Please click the button below to respond. +

+

+ VIEW + LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_counter_reminder.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_counter_reminder.text.erb new file mode 100644 index 000000000..4a781de93 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_counter_reminder.text.erb @@ -0,0 +1,3 @@ +<%= @student.name %> has proposed a different time 24 hours ago. Please respond. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.html.erb new file mode 100644 index 000000000..3c057778d --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.html.erb @@ -0,0 +1,50 @@ +<% provide(:title, @subject) %> + +

+ Hello <%= @name %>, +

+ + <% if @distribution.is_test_drive? %> + <% if @school %> +

We hope you enjoyed your TestDrive lesson today with <%= @distribution.student.name %>.

+ <% else %> +

You have earned <%= @distribution.real_distribution_display %> for your TestDrive lesson with <%= @distribution.student.name %>.

+ <% end %> +

+ <% if !@teacher_payment.teacher.has_rated_student(@distribution.student) %> + If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. + <% end %> + If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com. +

+ <% elsif @distribution.is_normal? %> + <% if @school %> +

we hope you enjoyed your lesson today with <%= @distribution.student.name %>.

+ <% else %> +

You have earned <%= @distribution.real_distribution_display %> for your lesson with <%= @distribution.student.name %>.

+ <% end %> +

+ <% if !@teacher_payment.teacher.has_rated_student(@distribution.student) %> + If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. + <% end %> + If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com. +

+ <% elsif @distribution.is_monthly? %> + <% if @school %> +

we hope you enjoyed your <%= @distribution.month_name %> lessons with <%= @distribution.student.name %>.

+ <% else %> +

You have earned <%= @distribution.real_distribution_display %> for your <%= @distribution.month_name%> lessons with <%= @distribution.student.name %>.

+ <% end %> +

+ <% if !@teacher_payment.teacher.has_rated_student(@distribution.student) %> + If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. + <% end %> + If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com. +

+ <% else %> + Unknown payment type. + <% end %> +
+
+ +Best Regards,
+JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.text.erb new file mode 100644 index 000000000..e8ce56e85 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.text.erb @@ -0,0 +1,40 @@ +<% provide(:title, @subject) %> + +Hello <%= @name %>, + +<% if @distribution.is_test_drive? %> +<% if @school %> +We hope you enjoyed your TestDrive lesson today with <%= @distribution.student.name %>. +<% else %> +You have earned <%= @distribution.real_distribution_display %> for your TestDrive lesson with <%= @distribution.student.name %>. +<% end %> +<% if !@teacher_payment.teacher.has_rated_student(@distribution.student) %> +If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= @distribution.student.student_ratings_url %> +<% end%> +If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com. +<% elsif @distribution.is_normal? %> +<% if @school %> +We hope you enjoyed your lesson today with <%= @distribution.student.name %>. +<% else %> +You have earned <%= @distribution.real_distribution_display %>for your lesson with <%= @distribution.student.name %>. +<% end %> +<% if !@teacher_payment.teacher.has_rated_student(@distribution.student) %> +If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= @distribution.student.student_ratings_url %> +<% end%> +If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com. +<% elsif @distribution.is_monthly? %> +<% if @school %> +We hope you enjoyed your <%= @distribution.month_name%> lessons with <%= @distribution.student.name %>. +<% else %> +You have earned <%= @distribution.real_distribution_display %>for your <%= @distribution.month_name%> lessons with <%= @distribution.student.name %>. +<% end %> +<% if !@teacher_payment.teacher.has_rated_student(@distribution.student) %> +If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= @distribution.student.student_ratings_url %> +<% end%> +If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com. +<% else %> +Unknown payment type. +<% end %> + +Best Regards, +JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.html.erb new file mode 100644 index 000000000..bcd797ffd --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, @subject) %> + +

Hello <%= @name %>,

+ +<% if @school %> +

+ We attempted to process a payment via your Stripe account for <%= @distribution.real_distribution_display %> for this lesson, but the payment failed. Please sign into your Stripe account, and verify that everything there is working properly. We’ll try again to process this payment in about 24 hours. +

+<% else %> +

+ <% if @card_declined %> + When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe. Can you please check your stripe account status? Thank you! + <% elsif @card_expired %> + When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe due to a card expiration. Can you please check your stripe account status? Thank you! + <% else %> + For some reason, when we tried to distribute a payment to you on <%= @bill_date %>, the charge failed. Can you please check your stripe account status? Thank you! + <% end %> +

+<% end %> +
+ +Best Regards,
+JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.text.erb new file mode 100644 index 000000000..a0ce26b67 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.text.erb @@ -0,0 +1,17 @@ +<% provide(:title, @subject) %> +Hello <%= @name %>, + +<% if @school %> + We attempted to process a payment via your Stripe account for <%= @distribution.real_distribution_display %> for this lesson, but the payment failed. Please sign into your Stripe account, and verify that everything there is working properly. We’ll try again to process this payment in about 24 hours. +<% else %> + <% if @card_declined %> +When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe. Can you please check your stripe account status? Thank you! + <% elsif @card_expired %> +When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe due to a card expiration. Can you please check your stripe account status? Thank you! + <% else %> +For some reason, when we tried to distribute a payment to you on <%= @bill_date %>, the charge failed. Can you please check your stripe account status? Thank you! + <% end %> +<% end %> + +Best Regards, +JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.html.erb new file mode 100644 index 000000000..ab838c208 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.html.erb @@ -0,0 +1,18 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_session.student.resolved_photo_url) %> + +<% content_for :note do %> +

+ <% if @slot.is_teacher_approved? %> + You have confirmed a lesson request. + <% else %> + This student has accepted your lesson request! + <% end %> + +

We strongly suggest adding this to your calendar so you don't forget it.

+

+ VIEW LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.text.erb new file mode 100644 index 000000000..6823a39f7 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.text.erb @@ -0,0 +1,3 @@ +You have confirmed a lesson request for <%= @sender.name %>. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.html.erb new file mode 100644 index 000000000..c0095a170 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.html.erb @@ -0,0 +1,29 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_booking.canceler.resolved_photo_url) %> + +<% content_for :note do %> +

+ <% if @lesson_booking.recurring %> + + All lessons that were scheduled for <%= @lesson_booking.dayWeekDesc %> with <%= @student.name %> have been canceled. + + <% else %> + Your lesson with <%= @student.name %> has been canceled.

+ + Session Name: <%= @session_name %>
+ Session Description: <%= @session_description %>
+ <%= @session_date %> + <% end %> + + <% if @message.present? %> +

<%= @lesson_booking.canceler.name %> says: +
<%= @message %> +
+ <% end %> +

Click the button below to view more info about the canceled session.

+

+ VIEW LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.text.erb new file mode 100644 index 000000000..c7506b0ad --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.text.erb @@ -0,0 +1,3 @@ +<%= @subject %> + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.html.erb new file mode 100644 index 000000000..4a60fe695 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.html.erb @@ -0,0 +1,24 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_session.canceler.resolved_photo_url) %> + +<% content_for :note do %> +

+ + Your lesson with <%= @teacher.name %> has been canceled.

+ + Session Name: <%= @session_name %>
+ Session Description: <%= @session_description %>
+ <%= @session_date %> + + <% if @message.present? %> +

<%= @lesson_session.canceler.name %> says: +
<%= @message %> +
+ <% end %> +

Click the button below to view more info about the canceled session.

+

+ VIEW LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.text.erb new file mode 100644 index 000000000..c7506b0ad --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.text.erb @@ -0,0 +1,3 @@ +<%= @subject %> + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.html.erb new file mode 100644 index 000000000..831a074bf --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.html.erb @@ -0,0 +1,20 @@ +<% provide(:title, "You successfully completed a lesson with #{@student.name}") %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

+ <% if @lesson_session.student_missed %> + Even though the student missed the lesson, + <% end %> + <%= @student.name %> will first be billed and you should receive your payment in the next 48 hours. +
+
+ Click the button below to see more information about this session. +

+

+ VIEW + LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.text.erb new file mode 100644 index 000000000..30e884e71 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.text.erb @@ -0,0 +1,3 @@ +You successfully completed a lesson with <%= @student.name %>. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.html.erb new file mode 100644 index 000000000..765f13b99 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.html.erb @@ -0,0 +1,17 @@ +<% provide(:title, "#{@student.name} has proposed a different time for their lesson") %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

+ <%= @student.name %> has proposed a different time for their lesson request. +
+
+ Click the button below to get more information and respond. +

+

+ VIEW + LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.text.erb new file mode 100644 index 000000000..050917a31 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.text.erb @@ -0,0 +1,3 @@ +<%= @student.name %> has proposed a different time for their lesson request. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.html.erb new file mode 100644 index 000000000..5b8bc3891 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @teacher.name %>, +

+ +

+ Your student, <%= @student.name %>, has been billed for this month's lessons. You should receive your funds within 48 hours. +

+ +

+ If you had technical problems during your lesson, or have questions, or would like to make suggestions + on how to improve JamClass, please email us at support@jamkazam.com. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.text.erb new file mode 100644 index 000000000..0347dc3be --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.text.erb @@ -0,0 +1,9 @@ +Hello <%= @teacher.name %>, + +Your student, <%= @student.name %>, has been billed for this month's lessons. You should receive your funds within 48 hours. + +If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com. + +Best Regards, +Team JamKazam + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.html.erb new file mode 100644 index 000000000..8ec5b345c --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, "Your JamClass lesson today with #{@student.first_name}") %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @teacher.name %>, +

+ +

+ Your student <%= @student.name %> will be billed for today's lesson, and you should receive payment within 48 hours. +

+ +

+ If you had technical problems during your lesson, or have questions, or would like to make suggestions + on how to improve JamClass, please email us at support@jamkazam.com. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.text.erb new file mode 100644 index 000000000..08db0f67e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.text.erb @@ -0,0 +1,11 @@ +Hello <%= @teacher.name %>, + +Your student <%= @student.name %> will be billed for today's lesson, and you will receive payment within 24 hours. + +If you had technical problems during your lesson, or have questions, or would like to make suggestions +on how to improve JamClass, please email us at support@jamkazam.com. + +Best Regards, +Team JamKazam + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.html.erb new file mode 100644 index 000000000..7b24c238e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, "Your student #{@student.name} will not be charged for their lesson") %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @teacher.name %>, +

+ +

Your student <%= @student.name %> will not be billed for today's session. +
+
+ Click the button below to see more information about this session. +

+

+ VIEW + LESSON DETAILS +

+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.text.erb new file mode 100644 index 000000000..9b9f3b91c --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.text.erb @@ -0,0 +1,5 @@ +Hello <%= @teacher.name %>, + +Your student <%= @student.name %> will not be billed for today's session. + +To see this lesson, click here: <%= @lesson_session.web_url %> diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.html.erb new file mode 100644 index 000000000..a82e118cb --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.html.erb @@ -0,0 +1,11 @@ +<% provide(:title, "Lesson Request from #{@sender.name}") %> +<% provide(:photo_url, @sender.resolved_photo_url) %> + +<% content_for :note do %> +

This student has requested to schedule a <%= @lesson_booking.display_type %> lesson.

Click the button below to get more information and to respond to this lesson request. You must respond to this lesson request promptly, or it will be cancelled, thank you!

+

+ VIEW LESSON REQUEST +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.text.erb new file mode 100644 index 000000000..c28cf0142 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.text.erb @@ -0,0 +1,3 @@ +<%= @sender.name %> has requested a lesson. + +To see this lesson request, click here: <%= @lesson_booking.home_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.html.erb new file mode 100644 index 000000000..ff9c4a3b6 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.html.erb @@ -0,0 +1,20 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_session.student.resolved_photo_url) %> + +<% content_for :note do %> +

+ All lessons with <%= @lesson_session.student.name %> have been rescheduled. + + <% if @message.present? %> +

<%= @sender.name %> says: +
<%= @message %> +
+ <% end %> +

Click the button below to get more information and to update your calendar! +

We strongly suggest adding this to your calendar so you don't forget it, or you'll end up paying for a lesson you don't get.

+

+ VIEW LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.text.erb new file mode 100644 index 000000000..5fc1c92a5 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.text.erb @@ -0,0 +1,3 @@ +All your lessons with <%= @lesson_session.student.name %> have been rescheduled. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.html.erb new file mode 100644 index 000000000..f192b7b9c --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.html.erb @@ -0,0 +1,11 @@ +<% provide(:title, @subject) %> + +

<%= @body %>

+ +

+ Session Name: <%= @session_name %>
+ <%= @session_description %>
+ <%= @session_date %> +

+ +

VIEW LESSON DETAILS

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.text.erb new file mode 100644 index 000000000..63679c0d2 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.text.erb @@ -0,0 +1,7 @@ +<%= @body %> + +<%= @session_name %> +<%= @session_description %> +<%= @session_date %> + +See session details at <%= @session_url %>. \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_test_drive_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_test_drive_no_bill.html.erb new file mode 100644 index 000000000..63b6ad7cb --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_test_drive_no_bill.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, "Your TestDrive with #{@student.name} was not successful") %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @teacher.name %>, +

+ +

Your TestDrive with <%= @student.name %> was not successful because <%= @lesson_session.error_display %>. +
+
+ Click the button below to see more information about this session. +

+

+ VIEW + LESSON DETAILS +

+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_test_drive_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_test_drive_no_bill.text.erb new file mode 100644 index 000000000..59a24b9d2 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_test_drive_no_bill.text.erb @@ -0,0 +1,5 @@ +Hello <%= @teacher.name %>, + +You have not used a credit for today's TestDrive with <%= @teacher.name %>. + +To see this lesson, click here: <%= @lesson_session.web_url %> diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.html.erb new file mode 100644 index 000000000..fbee04816 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.html.erb @@ -0,0 +1,21 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @teacher.name %>, +

+ +

+ <% if @lesson_booking.is_suspended? %> + Your weekly lessons with <%= @student.name %> have been suspended because we have tried repeatedly to charge their credit card but the charge was declined. They have been asked to re-enter updated credit card info. + <% else %> + We have tried to charge the credit card of <%= @student.name %> but the charge was declined. They still have time to re-enter their credit card info before the session is suspended, though. + <% end %> +

+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.text.erb new file mode 100644 index 000000000..8807cbbda --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.text.erb @@ -0,0 +1,14 @@ +Hello <%= @student.name %>, + +<%= @subject %> + +<% if @lesson_booking.is_suspended? %> + Your weekly lessons with <%= @student.name %> have been suspended because we have tried repeatedly to charge their credit card but the charge was declined. They have been asked to re-enter updated credit card info. +<% else %> + We have tried to charge the credit card of <%= @student.name %> but the charge was declined. They still have time to re-enter their credit card info before the session is suspended, though. +<% end %> + +Best Regards, +Team JamKazam + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.html.erb new file mode 100644 index 000000000..0590bfa1c --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.html.erb @@ -0,0 +1,61 @@ +<% provide(:title, @subject) %> + + +<% if !@user.anonymous? %> +

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +

+<% end %> + +

+ Thank you for signing up to teach online music lessons using the JamClass service by JamKazam. JamKazam technology was + built from the ground up for playing music live in sync with high quality audio from different locations over the + Internet, so you will find it delivers a massively better online music lesson platform than voice/chat apps like + Skype, etc. +

+ +

+ To get ready to teach JamClass students online, here are the things you'll want to do: +

+ + +

1. Set Up Your Teacher Profile
+ As JamKazam brings students into the JamClass marketplace, these students search for teachers. The way they find + teachers is by searching on their criteria (e.g. instruments, genres, etc.), and then by browsing through teacher + profiles to get a feel for the teachers who match their search criteria. Your teacher profile is critical to being + found in searches, and then presenting yourself in more depth to students who are interested in you. So you'll want to + take a little time to fill in the information in your teacher profile to present yourself well. + Click + here for + instructions on filling out your teacher profile. +

+ +

2. Set Up Your Gear
+ Click + here for information on the gear requirements to effectively teach using the JamClass service. When you have + everything you need, + use + this set of help articles as a good step-by-step guide to set up your gear for use with the + JamKazam application. After you have signed up, someone from JamKazam will contact you to schedule a test online + session, in which we will make sure your audio and video gear are working properly in an online session, and to make + sure you feel comfortable with the key features you will be using in sessions with students. +

+ +

3. Learn About JamClass Features
+ Click + this link for a set of help articles specifically for teachers to learn how to respond to student lesson + requests, how to join your lessons when they are scheduled to begin, how to get paid, and more. You can also + use + this + link for a set of help articles that explain how to use the key features available to you in online sessions to + effectively teach students. +

+ +

+ As you work through these things, if you ever get stuck or have questions, please don't hesitate to reach out for + help. You can email us any time at support@jamkazam.com. + We are happy to help you, and we look forward to helping you + reach and teach more students! +

+ +

Best Regards,
+ Team JamKazam

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.text.erb new file mode 100644 index 000000000..9702e24f5 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.text.erb @@ -0,0 +1,38 @@ +<% if !@user.anonymous? %>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +<% end %> + +Thank you for signing up to teach online music lessons using the JamClass service by JamKazam. JamKazam technology was +built from the ground up for playing music live in sync with high quality audio from different locations over the +Internet, so you will find it delivers a massively better online music lesson platform than voice/chat apps like +Skype, etc. + +To get ready to teach JamClass students online, here are the things you'll want to do: + +1. Set Up Your Teacher Profile +As JamKazam brings students into the JamClass marketplace, these students search for teachers. The way they find +teachers is by searching on their criteria (e.g. instruments, genres, etc.), and then by browsing through teacher +profiles to get a feel for the teachers who match their search criteria. Your teacher profile is critical to being +found in searches, and then presenting yourself in more depth to students who are interested in you. So you'll want to +take a little time to fill in the information in your teacher profile to present yourself well. +Click here for instructions on filling out your teacher profile. (https://jamkazam.desk.com/customer/en/portal/articles/2405835-creating-your-teacher-profile) + +2. Set Up Your Gear +Click here for information on the gear requirements to effectively teach using the JamClass service (https://jamkazam.desk.com/customer/en/portal/articles/1288274-computer-internet-audio-and-video-requirements). +When you have everything you need, use +this set of help articles as a good step-by-step guide to set up your gear for use with the +JamKazam application (https://jamkazam.desk.com/customer/en/portal/topics/930331-setting-up-your-gear-to-play-in-online-sessions/articles). +After you have signed up, someone from JamKazam will contact you to schedule a test online +session, in which we will make sure your audio and video gear are working properly in an online session, and to make +sure you feel comfortable with the key features you will be using in sessions with students. + +3. Learn About JamClass Features +Click this link for a set of help articles specifically for teachers to learn how to respond to student lesson +requests, how to join your lessons when they are scheduled to begin, how to get paid, and more (https://jamkazam.desk.com/customer/en/portal/topics/926076-jamclass-online-music-lessons---for-teachers/articles). +You can also use this link for a set of help articles that explain how to use the key features available to you in online sessions to +effectively teach students (https://jamkazam.desk.com/customer/en/portal/topics/673198-key-features-to-use-in-online-sessions/articles). + +As you work through these things, if you ever get stuck or have questions, please don't hesitate to reach out for help. You can email us any time at support@jamkazam.com. We are happy to help you, and we look forward to helping you reach and teach more students! + +Best Regards, +Team JamKazam + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb index 35e380618..8607f7bd5 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb @@ -1,63 +1,89 @@ <% provide(:title, 'Welcome to JamKazam!') %> +<% if !@user.anonymous? %>

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --

+<% end %> -

We're delighted that you have decided to try the JamKazam service, - and we hope that you will enjoy using JamKazam to play - music with others. - Following are some resources that can help you get oriented and get the most out of JamKazam. +

We're delighted to welcome you to the JamKazam community of musicians. Following are + + resources you can use to get the most out of JamKazam.

+

Teach or Take Online Music Lessons
+ Check out our user guide for students or our user guide for teachers to learn how you can take -

+ or teach online music lessons through our JamClass marketplace. Students can take lessons -

Getting Started
- There are basically three kinds of setups you can use to play on JamKazam.
-

    -
  • Built-In Audio on Your Computer - You can use a Windows or Mac computer, and just use the built-in mic and headphone jack to - handle your audio. This is cheap and easy, but your audio quality will suffer, and it will also process audio very slowly, - creating problems with latency, or lag, in your sessions. Still, you can at least start experimenting with JamKazam in this way.
  • -
  • Computer with External Audio Interface - You can use a Windows or Mac computer with an external audio interface that you - already own and use for recording, if you happen to have one already. If you are going to do this, or use the built-in mic/headphones on your computer, please refer - to our Minimum System Requirements - to make sure your computer will work. These requirements were on the download page for the app, but you may have sped by them. Also, we'd recommend watching our - Getting Started Video to learn more about your options here.
  • -
  • The JamBlaster - JamKazam has designed a new product from the ground up to be the best way to play music online in real time. It's called the JamBlaster. - It processes audio faster than any of the thousands of combinations of computers and interfaces in use on JamKazam today, which means you can play with musicians - who are farther away from you, and closer sessions will feel/sound tighter. The JamBlaster is both a computer and an audio interface, so it also eliminates the - system requirements worries, and it "just works" so you don't have to be an audio and computer genius to get it working. This is a great product - available only - through a Kickstarter program running during a 30-day window during parts of February and March 2015. You can watch the - JamBlaster Video to learn more about this amazing new product.
  • -
+ from the best teacher for you vs. the closest, test drive multiple teachers to see who you click + + with, avoid the time and hassle of travel to/from lessons, enjoy studio quality audio in online + + sessions, play live in sync with your teacher, and record lessons for later reference. Teachers + + can vastly increase your market of reachable students and let JamKazam bring students to you + + to help build your student base and income. Unlike Skype and similar apps, the JamKazam + + platform was built from the ground up for music, and it is the ideal technology for online music + + lessons.

-

JamKazam Features
- JamKazam offers a very robust and exciting set of features for playing online and sharing your performances with others. Here are some videos you can watch - to easily get up to speed on some of the things you can do with JamKazam:
-

+

Learn & Play Along With Your Favorite Music
+ + JamTracks by JamKazam are the best way to play along with your favorite songs. JamTracks are + + complete multi-track professional recordings, with fully isolated tracks for each part of the + + music. Mute any part. Slow down playback for practice. Change pitch/key up or down. Record + + yourself playing along with the rest of the band in audio or video, and more. Get your first + + JamTrack free to try one out! After that they are just $1.99 each. Click here for more + + information on how you can use JamTracks in your browser, in our free Mac or Windows + + desktop app, or in our free iOS app. +

+ +

Play Live And In Sync With Others from Different Locations
+ JamKazam’s free Mac and Windows desktop apps let musicians play together live and in sync + + with hiqh quality audio from different locations over the Internet. Great for band rehearsals, + + co-writing music, or just hopping into open jams with other musicians for fun. If you’re + + interested in playing with others online, check out our gear recommendations, instructions for + + setting up your gear, and help articles on how to set up your own online sessions or find and + + join others’ sessions, plus key features to use while in online sessions.

-

Getting Help
- If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to answer your questions and get you up and running. - You can visit our - Support Portal - to find knowledge base articles and post questions that have - not already been answered. You can email us at support@jamkazam.com. And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers, - you can visit our Community Forum - . +

And More...
+ + You can also generate income from our affiliate program by sharing info about JamKazam with + + your friends and followers online. Connect and network with other musicians. And more. If you + + run into trouble and need help, please reach out to us. We will be glad to do everything we can + + to answer your questions and get you the help you need. You can visit our support portal to + + find knowledge base articles and post questions that have not already been answered. You can + + email us at support@jamkazam.com. And if you just want to chat, share tips and war stories, + + and hang out with fellow JamKazam musicians, you can visit our community forum.

- Again, welcome to JamKazam, and we look forward to seeing – and hearing – you online soon! +
+
+ Again, welcome to JamKazam, and we hope you have a great time here!

Best Regards,
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb index a9f2ed06b..52dc8ab65 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb @@ -1,4 +1,4 @@ -Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +<% if !@user.anonymous? %>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --<% end %> We're delighted that you have decided to try the JamKazam service, and we hope that you will enjoy using JamKazam to play music with others. Following are some resources that can help you get oriented and get the most out of JamKazam. 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 5b7ce4aec..c80b0490c 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 @@ -5,13 +5,14 @@ JamKazam @@ -25,15 +26,22 @@ - + +

<%= yield(:title) %>

+

+ <%= yield(:title) %>

+

<%= yield %>


- +

+

+ +

<%= yield(:note) %>

-
+
@@ -43,24 +51,30 @@ <% unless @suppress_user_has_account_footer == true %> - - - - - + + +
+
+ + +
- -

-

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. +

- - + + <% end %> -
Copyright © <%= Time.now.year %> JamKazam, Inc. All rights reserved. + + Copyright © <%= Time.now.year %> JamKazam, + Inc. All rights reserved.
diff --git a/ruby/lib/jam_ruby/app/views/layouts/raw_mailer.html.erb b/ruby/lib/jam_ruby/app/views/layouts/raw_mailer.html.erb new file mode 100644 index 000000000..67ce3ee7f --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/layouts/raw_mailer.html.erb @@ -0,0 +1,72 @@ + + + + + JamKazam + + + + + + + + + + +
JamKazam
+ + + + + + + + + + <% unless @suppress_user_has_account_footer == true %> + + + + <% end %> +

+ <%= yield(:title) %>

+ + + <%= yield %> + + +
+ + + + +
+ + +

+

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

+ +
+ + + + +
+ Copyright © <%= Time.now.year %> JamKazam, + Inc. All rights reserved. +
+ + + diff --git a/ruby/lib/jam_ruby/app/views/layouts/raw_mailer.text.erb b/ruby/lib/jam_ruby/app/views/layouts/raw_mailer.text.erb new file mode 100644 index 000000000..dbc71434a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/layouts/raw_mailer.text.erb @@ -0,0 +1,7 @@ +<%= yield %> + +<% 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/base_manager.rb b/ruby/lib/jam_ruby/base_manager.rb index 5da4b3b0a..bf5dab694 100644 --- a/ruby/lib/jam_ruby/base_manager.rb +++ b/ruby/lib/jam_ruby/base_manager.rb @@ -2,7 +2,14 @@ module JamRuby class BaseManager attr_accessor :pg_conn - + + @@log = Logging.logger[BaseManager] + + # this is not working as expected + #@@in_websocket_gateway = Rails.env != 'test' && !Object.const_defined?(:UserManager) + + @@in_websocket_gateway = false + def initialize(options={}) @log = Logging.logger[self] @pg_conn = options[:conn] @@ -22,8 +29,19 @@ module JamRuby # across Rails ActiveRecord and the pg-gem based code in ConnectionManager. manager.pg_conn = connection.instance_variable_get("@connection") - connection.transaction do - yield manager + if @@in_websocket_gateway + # it only necessary to catch exceptions in websocket-gateway, which has only one AR connection and does not clean it up like a Rails context does + begin + connection.transaction do + yield manager + end + rescue Exception => e + ActiveRecord::Base.connection.execute('ROLLBACK') + end + else + connection.transaction do + yield manager + end end end end diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb index 352048dc0..92d960b22 100644 --- a/ruby/lib/jam_ruby/connection_manager.rb +++ b/ruby/lib/jam_ruby/connection_manager.rb @@ -28,13 +28,13 @@ module JamRuby ##### TODO: refactored to notification.rb but left here for backwards compatibility w/ connection_manager_spec.rb def gather_friends(connection, user_id) - friend_ids = [] - connection.exec("SELECT f1.friend_id as friend_id FROM friendships f1 WHERE f1.user_id = $1 AND f1.friend_id IN (SELECT f2.user_id FROM friendships f2 WHERE f2.friend_id = $1)", [user_id]) do |friend_results| - friend_results.each do |friend_result| - friend_ids.push(friend_result['friend_id']) - end + friend_ids = [] + connection.exec("SELECT f1.friend_id as friend_id FROM friendships f1 WHERE f1.user_id = $1 AND f1.friend_id IN (SELECT f2.user_id FROM friendships f2 WHERE f2.friend_id = $1)", [user_id]) do |friend_results| + friend_results.each do |friend_result| + friend_ids.push(friend_result['friend_id']) end - return friend_ids + end + return friend_ids end # this simulates music_session destroy callbacks with activerecord @@ -42,7 +42,7 @@ module JamRuby music_session = ActiveMusicSession.find_by_id(music_session_id) music_session.before_destroy if music_session end - + # reclaim the existing connection, if ip_address is not nil then perhaps a new address as well def reconnect(conn, channel_id, reconnect_music_session_id, ip_address, connection_stale_time, connection_expire_time, udp_reachable, gateway) music_session_id = nil @@ -65,11 +65,19 @@ module JamRuby isp = JamIsp.lookup(addr) #puts("============= JamIsp.lookup returns #{isp.inspect} for #{addr} =============") - if isp.nil? then ispid = 0 else ispid = isp.coid end + if isp.nil? then + ispid = 0 + else + ispid = isp.coid + end block = GeoIpBlocks.lookup(addr) #puts("============= GeoIpBlocks.lookup returns #{block.inspect} for #{addr} =============") - if block.nil? then locid = 0 else locid = block.locid end + if block.nil? then + locid = 0 + else + locid = block.locid + end location = GeoIpLocations.find_by_locid(locid) if location.nil? || isp.nil? || block.nil? @@ -167,7 +175,7 @@ SQL # this number is used by notification logic elsewhere to know # 'oh the user joined for the 1st time, so send a friend update', or # 'don't bother because the user has connected somewhere else already' - def create_connection(user_id, client_id, channel_id, ip_address, client_type, connection_stale_time, connection_expire_time, udp_reachable, gateway, &blk) + def create_connection(user_id, client_id, channel_id, ip_address, client_type, connection_stale_time, connection_expire_time, udp_reachable, gateway, is_jamblaster, &blk) # validate client_type raise "invalid client_type: #{client_type}" if client_type != 'client' && client_type != 'browser' @@ -183,11 +191,19 @@ SQL isp = JamIsp.lookup(addr) #puts("============= JamIsp.lookup returns #{isp.inspect} for #{addr} =============") - if isp.nil? then ispid = 0 else ispid = isp.coid end + if isp.nil? then + ispid = 0 + else + ispid = isp.coid + end block = GeoIpBlocks.lookup(addr) #puts("============= GeoIpBlocks.lookup returns #{block.inspect} for #{addr} =============") - if block.nil? then locid = 0 else locid = block.locid end + if block.nil? then + locid = 0 + else + locid = block.locid + end location = GeoIpLocations.find_by_locid(locid) if location.nil? || isp.nil? || block.nil? @@ -198,15 +214,18 @@ SQL lock_connections(conn) - conn.exec("INSERT INTO connections (user_id, client_id, channel_id, ip_address, client_type, addr, locidispid, aasm_state, stale_time, expire_time, udp_reachable, gateway) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", - [user_id, client_id, channel_id, ip_address, client_type, addr, locidispid, Connection::CONNECT_STATE.to_s, connection_stale_time, connection_expire_time, udp_reachable, gateway]).clear + conn.exec("INSERT INTO connections (user_id, client_id, channel_id, ip_address, client_type, addr, locidispid, aasm_state, stale_time, expire_time, udp_reachable, gateway, is_jamblaster) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", + [user_id, client_id, channel_id, ip_address, client_type, addr, locidispid, Connection::CONNECT_STATE.to_s, connection_stale_time, connection_expire_time, udp_reachable, gateway, is_jamblaster]).clear - # we just created a new connection-if this is the first time the user has shown up, we need to send out a message to his friends - conn.exec("SELECT count(user_id) FROM connections WHERE user_id = $1", [user_id]) do |result| - count = result.getvalue(0, 0) .to_i - # we're passing all this stuff so that the user record might be updated as well... - blk.call(conn, count) unless blk.nil? + if user_id + # we just created a new connection-if this is the first time the user has shown up, we need to send out a message to his friends + conn.exec("SELECT count(user_id) FROM connections WHERE user_id = $1", [user_id]) do |result| + count = result.getvalue(0, 0).to_i + # we're passing all this stuff so that the user record might be updated as well... + blk.call(conn, count) unless blk.nil? + end end + return count end end @@ -291,7 +310,7 @@ SQL # destroy the music_session if it's empty num_participants = nil - conn.exec("SELECT count(*) FROM connections WHERE music_session_id = $1", + conn.exec("SELECT count(*) FROM connections WHERE music_session_id = $1", [previous_music_session_id]) do |result| num_participants = result.getvalue(0, 0).to_i end @@ -324,22 +343,83 @@ SQL conn.exec("UPDATE active_music_sessions set jam_track_id = NULL, jam_track_initiator_id = NULL where jam_track_initiator_id = $1 and id = $2", [user_id, previous_music_session_id]) + + update_session_controller(previous_music_session_id) end end end + def update_session_controller(music_session_id) + active_music_session = ActiveMusicSession.find(music_session_id) + + if active_music_session + music_session = active_music_session.music_session + if music_session.session_controller_id && !active_music_session.users.exists?(music_session.session_controller) + # find next in line, because the current 'session controller' is not part of the session + next_in_line(music_session, active_music_session) + end + end + end + + # determine who should be session controller after someone leaves + def next_in_line(music_session, active_music_session) + session_users = active_music_session.users + + # check friends 1st + session_friends = music_session.creator.friends && session_users + if session_friends.length > 0 + music_session.session_controller = session_friends[0] + if music_session.save + active_music_session.tick_track_changes + Notification.send_tracks_changed(active_music_session) + return + end + end + + # check invited 2nd + invited = music_session.invited_musicians && session_users + if invited.length > 0 + music_session.session_controller = invited[0] + if music_session.save + active_music_session.tick_track_changes + Notification.send_tracks_changed(active_music_session) + return + end + end + + # go by who joined earliest + earliest = active_music_session.connections.order(:joined_session_at).first + + if earliest + music_session.session_controller = earliest + if music_session.save + active_music_session.tick_track_changes + Notification.send_tracks_changed(active_music_session) + return + end + end + music_session.creator + end + def join_music_session(user, client_id, music_session, as_musician, tracks, audio_latency, video_sources=nil) connection = nil ConnectionManager.active_record_transaction do |connection_manager| db_conn = connection_manager.pg_conn - connection = Connection.find_by_client_id_and_user_id!(client_id, user.id) + connection = Connection.find_by_client_id(client_id) + + if connection.nil? + raise JamRecordNotFound.new("Unable to find connection by client_id #{client_id}", 'Connection') + elsif connection.user_id.nil? + raise JamPermissionError, "no user_id associated with connection #{client_id}" + elsif connection.user_id != user.id + raise JamPermissionError, "wrong user_id associated with connection #{client_id}" + end connection.join_the_session(music_session, as_musician, tracks, user, audio_latency, video_sources) - - JamRuby::MusicSessionUserHistory.join_music_session(user.id, music_session.id) + JamRuby::MusicSessionUserHistory.join_music_session(user.id, music_session.id, client_id) # connection.music_session_id = music_session.id # connection.as_musician = as_musician # connection.joining_session = true @@ -349,12 +429,16 @@ SQL if connection.errors.any? raise ActiveRecord::Rollback + else + update_session_controller(music_session.id) end + end connection end + # if a blk is passed in, upon success, it will be called and you can issue notifications # within the connection table lock def leave_music_session(user, connection, music_session, &blk) @@ -383,6 +467,8 @@ SQL if result.cmd_tuples == 1 @log.debug("disassociated music_session with connection for client_id=#{client_id}, user_id=#{user_id}") + update_session_controller(music_session.id) + JamRuby::MusicSessionUserHistory.removed_music_session(user_id, music_session_id) session_checks(conn, previous_music_session_id, user_id) blk.call() unless blk.nil? diff --git a/ruby/lib/jam_ruby/constants/notification_types.rb b/ruby/lib/jam_ruby/constants/notification_types.rb index e05c8e00e..465d59e37 100644 --- a/ruby/lib/jam_ruby/constants/notification_types.rb +++ b/ruby/lib/jam_ruby/constants/notification_types.rb @@ -51,4 +51,10 @@ module NotificationTypes JAM_TRACK_SIGN_COMPLETE = "JAM_TRACK_SIGN_COMPLETE" JAM_TRACK_SIGN_FAILED = "JAM_TRACK_SIGN_FAILED" + MIXDOWN_SIGN_COMPLETE = "MIXDOWN_SIGN_COMPLETE" + MIXDOWN_SIGN_FAILED = "MIXDOWN_SIGN_FAILED" + + # jamclass + LESSON_MESSAGE = "LESSON_MESSAGE" + SCHEDULED_JAMCLASS_INVITATION = "SCHEDULED_JAMCLASS_INVITATION" end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/constants/validation_messages.rb b/ruby/lib/jam_ruby/constants/validation_messages.rb index 662fce608..99a2fa975 100644 --- a/ruby/lib/jam_ruby/constants/validation_messages.rb +++ b/ruby/lib/jam_ruby/constants/validation_messages.rb @@ -19,6 +19,8 @@ module ValidationMessages # sessions SESSION_NOT_FOUND = "Session not found." + NOT_FOUND = 'not found' + # genres RECORDING_GENRE_LIMIT_EXCEEDED = "No more than 1 genre is allowed." BAND_GENRE_LIMIT_EXCEEDED = "No more than 3 genres are allowed." @@ -39,6 +41,7 @@ module ValidationMessages EMAIL_ALREADY_TAKEN = "has already been taken" EMAIL_MATCHES_CURRENT = "is same as your current email" INVALID_FPFILE = "is not valid" + VERIFY_EMAIL = "You need to verify your email." # recurly RECURLY_ERROR = "Error occurred during Recurly transaction." diff --git a/ruby/lib/jam_ruby/errors/jam_record_not_found.rb b/ruby/lib/jam_ruby/errors/jam_record_not_found.rb new file mode 100644 index 000000000..d000f58d5 --- /dev/null +++ b/ruby/lib/jam_ruby/errors/jam_record_not_found.rb @@ -0,0 +1,12 @@ +module JamRuby + class JamRecordNotFound < StandardError + + attr_accessor :missing_message, :record_type + + def initialize(message, record_type) + @message = message + @missing_message = message + @record_type = record_type + end + 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 403c75330..21ce287c4 100644 --- a/ruby/lib/jam_ruby/jam_track_importer.rb +++ b/ruby/lib/jam_ruby/jam_track_importer.rb @@ -12,6 +12,7 @@ module JamRuby @@log = Logging.logger[JamTrackImporter] attr_accessor :name + attr_accessor :metadata attr_accessor :reason attr_accessor :detail attr_accessor :storage_format @@ -29,10 +30,307 @@ module JamRuby end def finish(reason, detail) + @@log.info("JamTrackImporter:#{self.name} #{reason} #{detail}") self.reason = reason self.detail = detail + + if ENV['END_ON_FAIL'] == "1" && reason != 'success' && reason != 'jam_track_exists' + raise "#{reason} #{detail}" + end end + def import_click_track(jam_track) + # we need to download the click track, if it exists. + Dir.mktmpdir do |tmp_dir| + + @@log.info("importing clicking track for #{jam_track.original_artist}:#{jam_track.name}") + + if jam_track.click_track + @@log.info("already has click track: #{jam_track.original_artist}:#{jam_track.name}") + finish('success', 'already_has_click_track') + return + end + + click_track_file = jam_track.click_track_file + if click_track_file.nil? + @@log.info("no click track for #{jam_track.original_artist}:#{jam_track.name}") + finish('success', 'no_click_track') + return + end + + original_filename = click_track_file[:original_filename] + + if original_filename.nil? + @@log.info("no click track s3 path for #{jam_track.original_artist}:#{jam_track.name}") + finish('no_original_source', 'click track is missing s3 path:' + click_track_file.id) + return + end + + wav_file = File.join(tmp_dir, File.basename(click_track_file[:original_filename])) + JamTrackImporter.song_storage_manager.download(click_track_file[:original_filename], wav_file) + + JamTrack.transaction do + click_track = jam_track.click_track + + if click_track.nil? + click_track = JamTrackTrack.new + click_track.original_filename = click_track_file[:original_filename] + click_track.original_audio_s3_path = click_track_file[:original_filename] + click_track.track_type = 'Click' + click_track.part = 'Clicktrack' + click_track.instrument_id = 'computer' + click_track.jam_track = jam_track + click_track.position = 10000 + click_track.wav_file = wav_file + + if !click_track.save + @@log.error("unable to create jamtrack click track #{click_track.errors.inspect}") + finish("jam_track_click", "unable to create: #{click_track.errors.inspect}") + return false + end + end + + jam_track.increment_version! + + # with the click track in hand, flesh out the details + synchronize_audio_track(jam_track, tmp_dir, false, click_track) + + finish('success', nil) + end + end + end + + def generate_jmep(jam_track) + + # https://docs.google.com/spreadsheets/d/1dyUOjWkeU8BXwnJl-ws1Kvxq_twWEG7E78F29haYkLc/edit#gid=987457683 + # cross-check against marks_approved + + if JamTrackImporter.marks_approved && JamTrackImporter.marks_approved.has_key?(jam_track.slug) + @@log.info("Found track in mark approved list. skipping") + finish('success', 'mark@jamkazam.com created') + return + end + + # can we overwrite this one? + if jam_track.jmep_text.blank? || jam_track.jmep_text.include?('created via code') + @@log.info("This is a blank jmep or created earlier by code.") + else + @@log.info("This a JMEP that was not created by code. Skip it.") + return + end + + # we need to download the click track, if it exists. + Dir.mktmpdir do |tmp_dir| + + master_track = jam_track.master_track + + click_track = jam_track.click_track_file + + if master_track.nil? + finish('no_master_track', nil) + return + end + + master_track_file = File.join(tmp_dir, File.basename(master_track[:url_48])) + begin + JamTrackImporter.private_s3_manager.download(master_track.url_by_sample_rate(44), master_track_file) + rescue Exception => e + @@log.error("unable to download master track") + finish("no-download-master", master_track.url_by_sample_rate(44)) + return + end + + if click_track + click_track_file = File.join(tmp_dir, File.basename(click_track[:original_filename])) + JamTrackImporter.song_storage_manager.download(click_track[:original_filename], click_track_file) + else + # we'll use the master for click analysis. not ideal, but would work + click_track_file = master_track_file + end + + + if click_track + start_time = determine_start_time(click_track_file, tmp_dir, click_track[:original_filename], false) + else + start_time = determine_start_time(master_track_file, tmp_dir, master_track[:url], false) + end + + trimmed_for_beat_analysis = File.join(tmp_dir, 'trimmed_for_beat.wav') + # trim out the 1st 5 second after non-silence + trim_cmd = "sox #{Shellwords.escape(click_track_file)} #{Shellwords.escape(trimmed_for_beat_analysis)} trim #{start_time} #{start_time + 5}" + cmd = "bash -c #{Shellwords.escape(trim_cmd)}" + @@log.debug("executing cmd #{cmd}") + output=`#{cmd}` + result_code = $?.to_i + + if result_code != 0 + finish("trim-fail", "failed to run trim click track: #{output}") + return + end + + # bpm comes from git clone http://www.pogo.org.uk/~mark/bpm-tools.git + sox="sox #{Shellwords.escape(trimmed_for_beat_analysis)} -t raw -r 44100 -e float -c 1 - | bpm -m 25 -x 250" + cmd = "bash -c #{Shellwords.escape(sox)}" + @@log.debug("executing cmd #{cmd}") + output=`#{cmd}` + result_code = $?.to_i + + if result_code == 0 + bpm = output.to_f + + offset = 0.140 + if bpm >= 60 && bpm < 80 + offset = 0.110 + elsif bpm >= 80 && bpm < 100 + offset = 0.080 + elsif bpm >= 100 && bpm < 120 + offset = 0.050 + elsif bpm >= 120 + offset = 0.020 + end + + @@log.debug("bpm: #{bpm} start_time: #{start_time}, offset: #{offset}") + + start_time += offset + metro_fin = "#{Time.at(start_time).utc.strftime("%H:%M:%S")}:#{((start_time - start_time.to_i) * 1000).round.to_s.rjust(3, "0")}" + + jmep = "" + jmep << "# created via code using bpm/silence detection (bpm:#{bpm} offset:#{offset})\r\n" + jmep << "prelude@10.0 #number of seconds before music starts\r\n" + jmep << "metro_fin@#{metro_fin} bpm=#{bpm}, ticks=8, pmode=stream, name=Beep, play=mono" + + @@log.info("jmep generated: #{jmep}") + + jam_track.jmep_text = jmep + if jam_track.save + finish('success', nil) + else + @@log.error("jamtrack did not save. #{jam_track.errors.inspect}") + finish("no-save", "jamtrack did not save. #{jam_track.errors.inspect}") + return + end + else + finish("bpm-fail", "failed to run bpm: #{output}") + return + end + end + end + + + def determine_start_time(audio_file, tmp_dir, original_filename, protect_short = true) + burp_gaps = ['0.3', '0.2', '0.1', '0.05', '0.025'] + + out_wav = File.join(tmp_dir, 'stripped.wav') + total_time_command = "soxi -D \"#{audio_file}\"" + total_time = `#{total_time_command}`.to_f + + result_code = -20 + stripped_time = total_time # default to the case where we just start the preview at the beginning + + burp_gaps.each do |gap| + command_strip_lead_silence = "sox \"#{audio_file}\" \"#{out_wav}\" silence 1 #{gap} 1%" + + @@log.debug("stripping silence: " + command_strip_lead_silence) + + output = `#{command_strip_lead_silence}` + + result_code = $?.to_i + + if result_code == 0 + stripped_time_command = "soxi -D \"#{out_wav}\"" + stripped_time_test = `#{stripped_time_command}`.to_f + + if stripped_time_test < 1 # meaning a very short duration + @@log.warn("could not determine the start of non-silence. assuming beginning") + stripped_time = total_time # default to the case where we just start the preview at the beginning + else + stripped_time = stripped_time_test # accept the measured time of the stripped file and move on by using break + break + end + else + @@log.warn("unable to determine silence for jam_track #{original_filename}, #{output}") + stripped_time = total_time # default to the case where we just start the preview at the beginning + end + end + + preview_start_time = total_time - stripped_time + + preview_start_time + end + + def synchronize_preview_dev(jam_track) + jam_track.jam_track_tracks.each do |track| + next if track.track_type != 'Master' + + + most_recent_aac = nil + most_recent_ogg = nil + most_recent_mp3 = nil + public_jamkazam_s3_manager.list_files(track.preview_directory).each do |s3_preview_item| + + s3_object = public_jamkazam_s3_manager.object(s3_preview_item) + + if s3_preview_item.end_with?('.aac') + if most_recent_aac + if s3_object.last_modified > most_recent_aac.last_modified + most_recent_aac = s3_object + end + else + most_recent_aac = s3_object + end + end + + if s3_preview_item.end_with?('.mp3') + if most_recent_mp3 + if s3_object.last_modified > most_recent_mp3.last_modified + most_recent_mp3 = s3_object + end + else + most_recent_mp3 = s3_object + end + end + + if s3_preview_item.end_with?('.ogg') + if most_recent_ogg + if s3_object.last_modified > most_recent_ogg.last_modified + most_recent_ogg = s3_object + end + else + most_recent_ogg = s3_object + end + end + end + + if most_recent_aac + track['preview_aac_md5'] = 'md5' + track['preview_aac_url'] = most_recent_aac.key + track['preview_aac_length'] = most_recent_aac.content_length + end + + if most_recent_mp3 + track['preview_mp3_md5'] = 'md5' + track['preview_mp3_url'] = most_recent_mp3.key + track['preview_mp3_length'] = most_recent_mp3.content_length + end + + if most_recent_ogg + track['preview_md5'] = 'md5' + track['preview_url'] = most_recent_ogg.key + track['preview_length'] = most_recent_ogg.content_length + end + + track.save + end + end + + def create_silence(tmp_dir, segment_count, duration, sample_rate, channels = 2) + file = File.join(tmp_dir, "#{segment_count}.wav") + + # -c 2 means stereo + cmd("sox -n -r #{sample_rate} -c #{channels} #{file} trim 0.0 #{duration}", "silence") + + file + end # this method was created due to Tency-sourced data having no master track # it goes through all audio tracks, and creates a master mix from it. (mix + normalize) @@ -116,7 +414,6 @@ module JamRuby end - temp_file = File.join(tmp_dir, "temp.wav") output_filename = JamTrackImporter.remove_s3_special_chars("#{self.name} Master Mix.wav") output_file = File.join(tmp_dir, output_filename) @@ -149,7 +446,8 @@ module JamRuby # now we need to upload the output back up s3_target = audio_path + '/' + output_filename @@log.debug("uploading #{output_file} to #{s3_target}") - JamTrackImporter.song_storage_manager.upload(s3_target, output_file ) + JamTrackImporter.song_storage_manager.upload(s3_target, output_file) + finish('success', nil) end @@ -159,6 +457,9 @@ module JamRuby end def dry_run(metadata, metalocation) + # STDIN.gets + + @@log.debug("dry_run: #{metadata.inspect}") metadata ||= {} parsed_metalocation = parse_metalocation(metalocation) @@ -195,7 +496,7 @@ module JamRuby meta[:licensor] = vendor - File.open(meta_yml, 'w') {|f| f.write meta.to_yaml } + File.open(meta_yml, 'w') { |f| f.write meta.to_yaml } jamkazam_s3_manager.upload(metalocation, meta_yml) end @@ -206,6 +507,26 @@ module JamRuby @storage_format == 'Tency' end + def is_helbing_storage? + assert_storage_set + @storage_format == 'Helbing' + end + + def is_paris_storage? + assert_storage_set + @storage_format == 'Paris' + end + + def is_tim_tracks_storage? + assert_storage_set + @storage_format == 'TimTracks' + end + + def is_drumma_storage? + assert_storage_set + @storage_format == 'Drumma' + end + def assert_storage_set raise "no storage_format set" if @storage_format.nil? end @@ -213,7 +534,30 @@ module JamRuby def parse_metalocation(metalocation) # metalocation = mapped/4 Non Blondes - What's Up - 6475/meta.yml - if is_tency_storage? + if is_drumma_storage? + + suffix = '/meta.yml' + + unless metalocation.end_with? suffix + finish("invalid_metalocation", "metalocation not valid #{metalocation}") + return nil + end + + metalocation = metalocation[0...-suffix.length] + + bits = ['audio'] + first_dash = metalocation.index(' - ') + if first_dash + artist = metalocation[0...(first_dash)].strip + bits << artist + else + finish("invalid_metalocation", "metalocation not valid #{metalocation}") + return nil + end + song = metalocation[(first_dash+3)..-1].strip + bits << song + + elsif is_tency_storage? || is_tim_tracks_storage? || is_helbing_storage? suffix = '/meta.yml' @@ -242,16 +586,70 @@ module JamRuby return nil end - last_dash = metalocation.rindex('-') - if last_dash - song = metalocation[(first_dash+3)...last_dash].strip + if is_tim_tracks_storage? + song = metalocation[(first_dash+3)..-1].strip bits << song - else + elsif is_helbing_storage? + song = metalocation[(first_dash+3)..-1].strip + bits << song + elsif is_tency_storage? + last_dash = metalocation.rindex('-') + if last_dash + song = metalocation[(first_dash+3)...last_dash].strip + bits << song + else + finish("invalid_metalocation", "metalocation not valid #{metalocation}") + return nil + end + end + + bits << 'meta.yml' + bits + elsif is_paris_storage? + suffix = '/meta.yml' + + unless metalocation.end_with? suffix finish("invalid_metalocation", "metalocation not valid #{metalocation}") return nil end + metalocation = metalocation[0...-suffix.length] + + first_path = metalocation.index('/') + if first_path.nil? + finish("invalid_metalocation", "metalocation not valid #{metalocation}") + return nil + end + metalocation = metalocation[(first_path + 1)..-1] + + bits = ['audio'] + + + last_slash = metalocation.rindex('/') + + # example: S4863-Mike Oldfield-Moonlight Shadow-000bpm + + + if last_slash + paris_artist_song_id = metalocation[0...last_slash] + else + paris_artist_song_id = metalocation + end + + + bitbits = paris_artist_song_id.split('-') + song_id = bitbits[0].strip + + artist = bitbits[1] + song_name = bitbits[2] + bpm = bitbits[-1] + + bits << artist + bits << song_name + bits << 'meta.yml' + bits << song_id + bits << bpm bits else bits = metalocation.split('/') @@ -275,24 +673,14 @@ module JamRuby end end - # if you change this, it will (at least without some work )break development usage of jamtracks - def gen_plan_code(original_artist, name) - # remove all non-alphanumeric chars from artist as well as name - artist_code = original_artist.gsub(/[^0-9a-z]/i, '').downcase - name_code = name.gsub(/[^0-9a-z]/i, '').downcase - "jamtrack-#{artist_code[0...20]}-#{name_code}"[0...50] # make sure it's a max of 50 long - end - def dry_run_metadata(metadata, original_artist, name) self.name = metadata["name"] || name original_artist = metadata["original_artist"] || original_artist - plan_code = metadata["plan_code"] || gen_plan_code(original_artist, self.name) description = metadata["description"] @@log.debug("#{self.name} original_artist=#{original_artist}") - @@log.debug("#{self.name} plan_code=#{plan_code}") true end @@ -309,7 +697,7 @@ module JamRuby genres << Genre.find('holiday') elsif genre == 'alternative' genres << Genre.find('alternative rock') - elsif genre == '80s' + elsif genre == '80s' || genre == "50's" || genre == "60's" || genre == "70's" || genre == "80's" || genre == "90's" || genre == "50/60's" || genre == "00's" || genre == "2010's" # swallow elsif genre == 'love' # swallow @@ -336,14 +724,62 @@ module JamRuby # swallow elsif genre == 'oriental' genres << Genre.find('asian') + elsif genre == 'abba' + genres << Genre.find('pop') + elsif genre == 'movies tv show' || genre == "movies" + genres << Genre.find('tv & movie soundtrack') + elsif genre == 'ballad' + # swallow + elsif genre == "r'n'b" || genre == "pop rnb" + genres << Genre.find("r&b") + elsif genre == "rock & roll" + genres << Genre.find('rock') + elsif genre == "dance pop" + genres << Genre.find('dance') + elsif genre == "soul/motown" || genre == "soul motown" + genres << Genre.find('soul') + elsif genre == "party" + # swallow + elsif genre == "reggae/ska" || genre == "reggae ska" + genres << Genre.find('reggae') + genres << Genre.find('ska') + elsif genre == "pop rock" || genre == "pop/rock" + genres << Genre.find("rock") + genres << Genre.find("pop") + elsif genre == "singalong" + #swallow + elsif genre == "folk rock" + genres << Genre.find('folk') + genres << Genre.find('rock') + elsif genre == "swing" || genre == "swing/big band" || genre == "swing big band" + genres << Genre.find('oldies') + elsif genre == "rap/hip hop" || genre == "rap hip hop" + genres << Genre.find("rap") + elsif genre == "folk traditional" || genre == "folk/traditional" + genres << Genre.find('folk') + elsif genre == "elvis" + genres << Genre.find('rock') + elsif genre == "irish" + genres << Genre.find('celtic') + elsif genre == "dance/pop" + genres << Genre.find('dance') + genres << Genre.find('pop') + elsif genre == "the beatles" + genres << Genre.find("rock") else found = Genre.find_by_id(genre) - genres << found if found + genres << found if found end end end + # just throw them into rock/pop if not known. can fix later... + if genres.length == 0 + genres << Genre.find('rock') + genres << Genre.find('pop') + end + genres end @@ -408,7 +844,7 @@ module JamRuby prevent_concurrent_processing(metalocation) if jam_track.new_record? - latest_jamtrack = JamTrack.order('created_at desc').first + latest_jamtrack = JamTrack.order('id::int desc').first id = latest_jamtrack.nil? ? 1 : latest_jamtrack.id.to_i + 1 if ENV['NODE_NUMBER'] @@ -418,9 +854,9 @@ module JamRuby node_count = ENV['NODE_COUNT'].to_i raise "NO NODE_COUNT" if node_count == 0 r = id % node_count - id = r + id # get to the same base number if both are working at the same time + id = (node_count - r) + id # get to the same base number if both are working at the same time id = id + node_number # offset by your node number - @@log.debug("JAM TRACK ID: #{id}") + @@log.info("JAM TRACK ID: #{id}") end 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' @@ -431,7 +867,6 @@ module JamRuby jam_track.year = metadata[:year] jam_track.genres = determine_genres(metadata) jam_track.language = determine_language(metadata) - jam_track.plan_code = metadata["plan_code"] || gen_plan_code(jam_track.original_artist, jam_track.name) jam_track.price = 1.99 jam_track.reproduction_royalty_amount = nil jam_track.reproduction_royalty = true @@ -444,13 +879,35 @@ module JamRuby jam_track.alternative_license_status = false jam_track.hfa_license_desired = true jam_track.server_fixation_date = Time.now - jam_track.slug = metadata['slug'] || jam_track.generate_slug - if is_tency_storage? jam_track.vendor_id = metadata[:id] - jam_track.licensor = JamTrackLicensor.find_by_name('Tency Music') + jam_track.licensor = JamTrackLicensor.find_by_name!('Tency Music') #add_licensor_metadata('Tency Music', metalocation) + elsif is_paris_storage? + raise 'no vendor id' if metadata[:id].nil? + jam_track.vendor_id = metadata[:id] + jam_track.licensor = JamTrackLicensor.find_by_name!('Paris Music') + jam_track.bpm = metadata[:bpm] + elsif is_tim_tracks_storage? + jam_track.vendor_id = metadata[:id] + jam_track.licensor = JamTrackLicensor.find_by_name!('Tim Waurick') + elsif is_helbing_storage? + jam_track.vendor_id = metadata[:id] + jam_track.licensor = JamTrackLicensor.find_by_name!('Stockton Helbing') + elsif is_drumma_storage? + jam_track.vendor_id = metadata[:id] + jam_track.licensor = JamTrackLicensor.find_by_name!('Drumma Boy') end + jam_track.slug = metadata['slug'] + if jam_track.slug.nil? + jam_track.generate_slug + end + jam_track.plan_code = metadata["plan_code"] + if jam_track.plan_code.nil? + jam_track.gen_plan_code + end + + else if !options[:resync_audio] #@@log.debug("#{self.name} skipped because it already exists in database") @@ -511,8 +968,11 @@ module JamRuby instrument = 'acoustic guitar' elsif potential_instrument == 'acoutic guitar' instrument = 'electric guitar' - elsif potential_instrument == 'electric gutiar' || potential_instrument == 'electric guitat' || potential_instrument == 'electric guitary' + elsif potential_instrument == 'electric gutiar' || potential_instrument == 'electric guitat' || potential_instrument == 'electric guitary' || potential_instrument == 'elec guitar' instrument = 'electric guitar' + elsif potential_instrument == 'lead guitar' + instrument = 'electric guitar' + part = 'Lead' elsif potential_instrument == 'keys' instrument = 'keyboard' elsif potential_instrument == 'vocal' || potential_instrument == 'vocals' @@ -555,7 +1015,7 @@ module JamRuby instrument = 'computer' part = 'Bells' elsif potential_instrument == 'percussion' - instrument = 'drums' + instrument = 'percussion' part = 'Percussion' elsif potential_instrument == 'fretless bass' instrument = 'bass guitar' @@ -583,8 +1043,9 @@ module JamRuby elsif potential_instrument == 'strings' instrument = 'orchestra' part = 'Strings' - elsif potential_instrument == 'celesta' + elsif potential_instrument == 'celesta' || potential_instrument == 'celeste' instrument = 'keyboard' + part = 'Celesta' elsif potential_instrument == 'balalaika' instrument = 'other' part = 'Balalaika' @@ -598,8 +1059,11 @@ module JamRuby instrument = 'other' part = 'Bouzouki' elsif potential_instrument == 'claps' || potential_instrument == 'hand claps' - instrument = 'computer' + instrument = 'other' part = 'Claps' + elsif potential_instrument == 'snaps' || potential_instrument == 'snap' + instrument = 'other' + part = 'Snaps' else found_instrument = Instrument.find_by_id(potential_instrument) if found_instrument @@ -631,7 +1095,7 @@ module JamRuby part = nil precount_num = nil no_precount_detail = nil - if comparable_filename == "click" || comparable_filename.include?("clicktrack") + if comparable_filename == "click" || comparable_filename.include?("clicktrack") || comparable_filename.include?("click track") || comparable_filename.end_with?('click') || comparable_filename.end_with?('click trac') if filename.end_with?('.txt') type = :clicktxt else @@ -650,8 +1114,7 @@ module JamRuby precount_num = precount.to_i end - - elsif comparable_filename.include?("master mix") || comparable_filename.include?("mastered mix") + elsif comparable_filename.include?("master mix") || comparable_filename.include?("mastered mix") || (@metadata && (@metadata[:id] && comparable_filename.start_with?(@metadata[:id].downcase))) master = true type = :master else @@ -666,6 +1129,9 @@ module JamRuby unless stem_location stem_location = comparable_filename.index('stems-') end + unless stem_location + stem_location = comparable_filename.index(' -') + end if stem_location bits = filename_no_ext[stem_location..-1].split('-') @@ -690,6 +1156,14 @@ module JamRuby result = determine_instrument(possible_instrument, possible_part) instrument = result[:instrument] part = result[:part] + + + if is_tim_tracks_storage? + if instrument.nil? + # some stems don't include 'voice'. but... they are all voice :) + instrument = 'voice' + end + end else if is_tency_storage? # we can check to see if we can find mapping info for this filename @@ -706,24 +1180,81 @@ module JamRuby instrument = result[:instrument] part = result[:part] end - end - end + elsif is_paris_storage? + # example: Eternal Flame-Guide Lead Vocal.wav + # or with a part: Eternal Flame-Keyboard-Stab.wav + bits = filename_no_ext.split('-') + bits.collect! { |bit| bit.strip } + while true + instrument, part = paris_instrument_parse(bits) + + if instrument.nil? && bits.length > 2 + bits.shift + instrument, part = paris_instrument_parse(bits) + else + break + end + end + end + + end end - {filename: filename, master: master, instrument: instrument, part: part, type: type, precount_num: precount_num, no_precount_detail: no_precount_detail} end + def paris_instrument_parse(bits) + instrument = nil + part = nil + possible_instrument = nil + possible_part = nil + + if bits.length == 2 + # second bit is instrument + possible_instrument = bits[1] + elsif bits.length == 3 + # second bit is instrument, third bit is part + possible_instrument = bits[1] + possible_part = bits[2] + elsif bits.length >= 4 + possible_instrument = bits[1] + possible_part = "#{bits[2]} #{bits[3]}" + end + + # otherwise, try mapping + if instrument.nil? && possible_instrument + mapping = JamTrackImporter.paris_mapping[possible_instrument.downcase] + if mapping + instrument = mapping[:instrument].downcase + part = mapping[:part] + part = nil if part.blank? + end + end + + # paris mapping didn't work; let's retry one more time with our own home-grown mapping + if instrument.nil? + result = determine_instrument(possible_instrument, possible_part) + instrument = result[:instrument] + part = result[:part] + end + return instrument, part + end + def dry_run_audio(metadata, s3_path) all_files = fetch_important_files(s3_path) + masters = 0 all_files.each do |file| # ignore click/precount parsed_wav = parse_file(file) if parsed_wav[:master] @@log.debug("#{self.name} master! filename: #{parsed_wav[:filename]}") + masters += 1 + if masters > 1 + JamTrackImporter.summaries[:multiple_masters] += 1 + end elsif parsed_wav[:type] == :track JamTrackImporter.summaries[:total_tracks] += 1 @@ -770,72 +1301,77 @@ module JamRuby instrument_weight = nil # if there are any persisted tracks, do not sort from scratch; just stick new stuff at the end - if track.persisted? - instrument_weight = track.position - else - if track.instrument_id == 'voice' + #if track.persisted? + # instrument_weight = track.position + #else + if track.instrument_id == 'voice' - if track.part && track.part.start_with?('Lead') - instrument_weight = 100 - elsif track.part && track.part.start_with?('Backing') - instrument_weight = 110 - else - instrument_weight = 120 - end - - elsif track.instrument_id == 'drums' - - if track.part && track.part == 'Drums' - instrument_weight = 150 - elsif track.part && track.part == 'Percussion' - instrument_weight = 160 - else - instrument_weight = 170 - end - - elsif track.instrument_id == 'bass guitar' && track.part && track.part == 'Bass' - instrument_weight = 180 - - elsif track.instrument_id == 'piano' && track.part && track.part == 'Piano' - instrument_weight = 250 - - elsif track.instrument_id == 'keyboard' - - if track.part && track.part.start_with?('Synth') - instrument_weight = 260 - elsif track.part && track.part.start_with?('Pads') - instrument_weight = 270 - else - instrument_weight = 280 - end - - elsif track.instrument_id == 'acoustic guitar' - if track.part && track.part.start_with?('Lead') - instrument_weight = 300 - elsif track.part && track.part.start_with?('Rhythm') - instrument_weight = 310 - else - instrument_weight = 320 - end - elsif track.instrument_id == 'electric guitar' - if track.part && track.part.start_with?('Lead') - instrument_weight = 400 - elsif track.part && track.part.start_with?('Solo') - instrument_weight = 410 - elsif track.part && track.part.start_with?('Rhythm') - instrument_weight = 420 - else - instrument_weight = 440 - end + if track.part && track.part.start_with?('Lead') + instrument_weight = 100 + elsif track.part && track.part.start_with?('Backing') + instrument_weight = 110 else - instrument_weight = slop + instrument_weight = 120 end - if track.track_type == 'Master' - instrument_weight = 1000 + elsif track.instrument_id == 'drums' + + if track.part && track.part == 'Drums' + instrument_weight = 150 + elsif track.part && track.part == 'Percussion' + instrument_weight = 160 + else + instrument_weight = 170 end + elsif track.instrument_id == 'percussion' + instrument_weight = 175 + elsif track.instrument_id == 'bass guitar' && track.part && track.part == 'Bass' + instrument_weight = 180 + + elsif track.instrument_id == 'piano' && track.part && track.part == 'Piano' + instrument_weight = 250 + + elsif track.instrument_id == 'keyboard' + + if track.part && track.part.start_with?('Synth') + instrument_weight = 260 + elsif track.part && track.part.start_with?('Pads') + instrument_weight = 270 + else + instrument_weight = 280 + end + + elsif track.instrument_id == 'acoustic guitar' + if track.part && track.part.start_with?('Lead') + instrument_weight = 300 + elsif track.part && track.part.start_with?('Rhythm') + instrument_weight = 310 + else + instrument_weight = 320 + end + elsif track.instrument_id == 'electric guitar' + if track.part && track.part.start_with?('Lead') + instrument_weight = 400 + elsif track.part && track.part.start_with?('Solo') + instrument_weight = 410 + elsif track.part && track.part.start_with?('Rhythm') + instrument_weight = 420 + else + instrument_weight = 440 + end + else + instrument_weight = slop end + if track.track_type == 'Master' + instrument_weight = 1000 + end + + if track.track_type == 'Click' + instrument_weight = 10000 + end + #end + instrument_weight end @@ -895,26 +1431,70 @@ module JamRuby end 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| - 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 - - + track.position = position + position = position + 1 end - sorted_tracks[sorted_tracks.length - 1].position = 1000 + # get click/master tracks position re-set correctly + + last_track = sorted_tracks[sorted_tracks.length - 1] + second_to_last = sorted_tracks[sorted_tracks.length - 2] + + if last_track.track_type == 'Master' + last_track.position = 1000 + elsif last_track.track_type == 'Click' + last_track.position = 10000 + end + + if second_to_last.track_type == 'Master' + second_to_last.position = 1000 + elsif second_to_last.track_type == 'Click' + second_to_last.position = 10000 + end sorted_tracks end + # this will put original_audio_s3_path on each jam_track_track + def associate_tracks_with_original_stems(jam_track, s3_path) + attempt_to_match_existing_tracks = true + + # find all wav files in the JamTracks s3 bucket + wav_files = fetch_important_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 + end + + tracks + end + def synchronize_audio(jam_track, metadata, s3_path, skip_audio_upload) attempt_to_match_existing_tracks = true @@ -952,52 +1532,9 @@ module JamRuby @@log.debug("no existing track found; creating a new one") - track = JamTrackTrack.new - track.original_filename = wav_file - track.original_audio_s3_path = wav_file - - file = JamTrackFile.new - file.original_filename = wav_file - file.original_audio_s3_path = wav_file - - parsed_wav = parse_file(wav_file) - - unknowns = 0 - if parsed_wav[:master] - track.track_type = 'Master' - track.part = 'Master Mix' - track.instrument_id = 'computer' - tracks << track - @@log.debug("#{self.name} master! filename: #{parsed_wav[:filename]}") - elsif parsed_wav[:type] == :track - - if !parsed_wav[:instrument] || !parsed_wav[:part] - @@log.warn("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") - unknowns += 1 - else - @@log.debug("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") - end - - - track.instrument_id = parsed_wav[:instrument] || 'other' - track.track_type = 'Track' - track.part = parsed_wav[:part] || "Other #{unknowns}" - tracks << track - elsif parsed_wav[:type] == :clicktxt - file.file_type = 'ClickTxt' - addt_files << file - elsif parsed_wav[:type] == :clickwav - file.file_type = 'ClickWav' - addt_files << file - elsif parsed_wav[:type] == :precount - file.file_type = 'Precount' - file.precount_num = parsed_wav[:precount_num] - addt_files << file - else - finish("unknown_file_type", "unknown file type #{wave_file}") + if !assign_instrument_parts(wav_file, tracks, addt_files) return false end - end jam_track.jam_track_tracks.each do |jam_track_track| @@ -1033,103 +1570,136 @@ module JamRuby return synchronize_audio_files(jam_track, skip_audio_upload) end + def reassign_instrument_parts(jam_track) + + tracks = [] + addt_files = [] + + jam_track.jam_track_tracks.each do |track| + return if !assign_instrument_parts(track.original_filename, tracks, addt_files, true) + end + + @@log.info("sorting #{tracks.length} tracks") + tracks = sort_tracks(tracks) + + deduplicate_parts(tracks) + + changed = false + tracks.each do |track| + if track.changed? + changed = true + puts "CHANGE: #{track.changes.inspect}" + track.skip_inst_part_uniq = true + track.save! + end + end + + if changed + # if we messed up any instrument/parts by making a dup, this will catch it + tracks.each do |track| + track.skip_inst_part_uniq = false + track.save! + end + end + end + + + + def assign_instrument_parts(wav_file, tracks, addt_files, reassign = false) + if !reassign + track = JamTrackTrack.new + track.original_filename = wav_file + track.original_audio_s3_path = wav_file + + file = JamTrackFile.new + file.original_filename = wav_file + file.original_audio_s3_path = wav_file + else + matches = JamTrackTrack.where(original_filename: wav_file) + if matches.count > 1 + raise "multiple jam track tracks encountered with #{wav_file} as original_filename" + elsif matches.count == 0 + raise "unable to locate jam track wit h#{wav_file} as original_filename" + end + track = matches[0] + track.original_audio_s3_path = wav_file + file = nil + end + + parsed_wav = parse_file(wav_file) + + unknowns = 0 + if parsed_wav[:master] + track.track_type = 'Master' + track.part = 'Master Mix' + track.instrument_id = 'computer' + tracks << track + @@log.debug("#{self.name} master! filename: #{parsed_wav[:filename]}") + elsif parsed_wav[:type] == :track + + if !parsed_wav[:instrument] || !parsed_wav[:part] + @@log.warn("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") + unknowns += 1 + else + @@log.debug("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") + end + + track.instrument_id = parsed_wav[:instrument] || 'other' + track.track_type = 'Track' + track.part = parsed_wav[:part]; + tracks << track + elsif parsed_wav[:type] == :clicktxt + file.file_type = 'ClickTxt' + addt_files << file + elsif parsed_wav[:type] == :clickwav + if file + file.file_type = 'ClickWav' + addt_files << file + end + + # and also add a JamTrackTrack for this click track + track.track_type = 'Click' + track.part = 'Clicktrack' + track.instrument_id = 'computer' + track.position = 10000 + tracks << track + elsif parsed_wav[:type] == :precount + file.file_type = 'Precount' + file.precount_num = parsed_wav[:precount_num] + addt_files << file + else + finish("unknown_file_type", "unknown file type #{wave_file}") + return false + end + + return true + end + def synchronize_audio_files(jam_track, skip_audio_upload) begin Dir.mktmpdir do |tmp_dir| + # download each jam track here, and then do processing to determine: + # what's the longest stem + # and to then pad the rest of the tracks to make them all match in length jam_track.jam_track_tracks.each do |track| - basename = File.basename(track.original_audio_s3_path) - s3_dirname = File.dirname(track.original_audio_s3_path) + wav_file = File.join(tmp_dir, basename) - # make a 44100 version, and a 48000 version - ogg_44100_filename = File.basename(basename, ".wav") + "-44100.ogg" - ogg_48000_filename = File.basename(basename, ".wav") + "-48000.ogg" + # bring the original wav file down from S3 to local file system + JamTrackImporter::song_storage_manager.download(track.original_audio_s3_path, wav_file) + track.wav_file = wav_file + end - ogg_44100_s3_path = track.filename(ogg_44100_filename) - ogg_48000_s3_path = track.filename(ogg_48000_filename) + same_lengthening(jam_track, tmp_dir) - track.skip_uploader = true - - if skip_audio_upload - track["url_44"] = ogg_44100_s3_path - track["md5_44"] = 'md5' - track["length_44"] = 1 - - track["url_48"] = ogg_48000_s3_path - track["md5_48"] = 'md5' - track["length_48"] = 1 - - # we can't fake the preview as easily because we don't know the MD5 of the current item - #track["preview_md5"] = 'md5' - #track["preview_mp3_md5"] = 'md5' - #track["preview_url"] = track.preview_filename('md5', 'ogg') - #track["preview_length"] = 1 - #track["preview_mp3_url"] = track.preview_filename('md5', 'mp3') - #track["preview_mp3_length"] = 1 - #track["preview_start_time"] = 0 - else - wav_file = File.join(tmp_dir, basename) - - # bring the original wav file down from S3 to local file system - JamTrackImporter::song_storage_manager.download(track.original_audio_s3_path, wav_file) - - sample_rate = `soxi -r "#{wav_file}"`.strip - - ogg_44100 = File.join(tmp_dir, ogg_44100_filename) - ogg_48000 = File.join(tmp_dir, File.basename(basename, ".wav") + "-48000.ogg") - - if sample_rate == "44100" - `oggenc "#{wav_file}" -q 6 -o "#{ogg_44100}"` - else - `oggenc "#{wav_file}" --resample 44100 -q 6 -o "#{ogg_44100}"` - end - - if sample_rate == "48000" - `oggenc "#{wav_file}" -q 6 -o "#{ogg_48000}"` - else - `oggenc "#{wav_file}" --resample 48000 -q 6 -o "#{ogg_48000}"` - end - - # upload the new ogg files to s3 - @@log.debug("uploading 44100 to #{ogg_44100_s3_path}") - - jamkazam_s3_manager.upload(ogg_44100_s3_path, ogg_44100) - - @@log.debug("uploading 48000 to #{ogg_48000_s3_path}") - - jamkazam_s3_manager.upload(ogg_48000_s3_path, ogg_48000) - - ogg_44100_digest = ::Digest::MD5.file(ogg_44100) - # and finally update the JamTrackTrack with the new info - track["url_44"] = ogg_44100_s3_path - track["md5_44"] = ogg_44100_digest.hexdigest - track["length_44"] = File.new(ogg_44100).size - - track["url_48"] = ogg_48000_s3_path - track["md5_48"] = ::Digest::MD5.file(ogg_48000).hexdigest - 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 - if track.track_type == 'Master' - preview_succeeded = synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_44100_digest) - - if !preview_succeeded - return false - end - elsif track.track_type == 'Track' - synchronize_track_preview(track, tmp_dir, ogg_44100) - end - - end - - track.save! + jam_track.jam_track_tracks.each do |track| + synchronize_audio_track(jam_track, tmp_dir, skip_audio_upload, track) end end + + generate_jmep(jam_track) rescue Exception => e finish("sync_audio_exception", e.to_s) return false @@ -1138,6 +1708,287 @@ module JamRuby return true end + # make all stems be the same length + def same_lengthening(jam_track, tmp_dir) + longest_duration = nil + jam_track.jam_track_tracks.each do |track| + duration_command = "soxi -D \"#{track.wav_file}\"" + output = `#{duration_command}` + + result_code = $?.to_i + + if result_code == 0 + duration = output.to_f.round + + track.tmp_duration = duration + if longest_duration.nil? + longest_duration = duration + else + if duration > longest_duration + longest_duration = duration + end + end + else + @@log.warn("unable to determine duration for jam_track track #{jam_track.name} #{jam_track_track.instrument} #{jam_track_track.part}. output #{output}") + end + end + + @@log.info("duration determined to be #{longest_duration}") + jam_track.duration = longest_duration + jam_track.jam_track_tracks.each do |track| + if track.tmp_duration < longest_duration + # need to pad with silence to make all match in length + + amount = longest_duration - track.tmp_duration + + @@log.info("track #{track.instrument_id}:#{track.part} needs to be lengthened by #{amount}") + + output = cmd("soxi -c \"#{track.wav_file}\"", "padded_silence") + channels = output.to_i + + output = cmd("soxi -r \"#{track.wav_file}\"", "get_sample_rate") + sample_rate = output.to_i + + padding_file = create_silence(tmp_dir, "padded_silence#{track.id}", amount, sample_rate, channels) + + output_file = File.join(tmp_dir, "with_padding_#{track.id}.wav") + + cmd("sox \"#{track.wav_file}\" \"#{padding_file}\" \"#{output_file}\"", "same_lengthening") + + track.wav_file = output_file + end + end + end + + def cmd(cmd, type) + + @@log.debug("executing #{cmd}") + + output = `#{cmd}` + + result_code = $?.to_i + + if result_code == 0 + output + else + @error_reason = type + "_fail" + @error_detail = "#{cmd}, #{output}" + raise "command `#{cmd}` failed. #{type}, #{output}" + end + end + + def synchronize_audio_track(jam_track, tmp_dir, skip_audio_upload, track) + basename = File.basename(track.original_audio_s3_path) + + # make a 44100 version, and a 48000 version + ogg_44100_filename = File.basename(basename, ".wav") + "-44100.ogg" + ogg_48000_filename = File.basename(basename, ".wav") + "-48000.ogg" + + + # make a 44100 version, and a 48000 version + mp3_48000_filename = File.basename(basename, ".wav") + "-48000.mp3" + aac_48000_filename = File.basename(basename, ".wav") + "-48000.aac" + + ogg_44100_s3_path = track.filename(ogg_44100_filename) + ogg_48000_s3_path = track.filename(ogg_48000_filename) + + mp3_48000_s3_path = track.filename(mp3_48000_filename) + aac_48000_s3_path = track.filename(aac_48000_filename) + + track.skip_uploader = true + + if skip_audio_upload + track["url_44"] = ogg_44100_s3_path + track["md5_44"] = 'md5' + track["length_44"] = 1 + + track["url_48"] = ogg_48000_s3_path + track["md5_48"] = 'md5' + track["length_48"] = 1 + + track["url_mp3_48"] = mp3_48000_filename + track["md5_mp3_48"] = 'md5' + track["length_mp3_48"] = 1 + + track["url_aac_48"] = aac_48000_filename + track["md5_aac_48"] = 'md5' + track["length_aac_48"] = 1 + + # we can't fake the preview as easily because we don't know the MD5 of the current item + #track["preview_md5"] = 'md5' + #track["preview_mp3_md5"] = 'md5' + #track["preview_url"] = track.preview_filename('md5', 'ogg') + #track["preview_length"] = 1 + #track["preview_mp3_url"] = track.preview_filename('md5', 'mp3') + #track["preview_mp3_length"] = 1 + #track["preview_start_time"] = 0 + else + wav_file = track.wav_file + + sample_rate = `soxi -r "#{wav_file}"`.strip + + ogg_44100 = File.join(tmp_dir, ogg_44100_filename) + ogg_48000 = File.join(tmp_dir, File.basename(basename, ".wav") + "-48000.ogg") + + if sample_rate == "44100" + `oggenc "#{wav_file}" -q 6 -o "#{ogg_44100}"` + else + `oggenc "#{wav_file}" --resample 44100 -q 6 -o "#{ogg_44100}"` + end + + if sample_rate == "48000" + `oggenc "#{wav_file}" -q 6 -o "#{ogg_48000}"` + else + `oggenc "#{wav_file}" --resample 48000 -q 6 -o "#{ogg_48000}"` + end + + # upload the new ogg files to s3 + @@log.debug("uploading 44100 to #{ogg_44100_s3_path}") + + jamkazam_s3_manager.upload(ogg_44100_s3_path, ogg_44100) + + @@log.debug("uploading 48000 to #{ogg_48000_s3_path}") + + jamkazam_s3_manager.upload(ogg_48000_s3_path, ogg_48000) + + ogg_44100_digest = ::Digest::MD5.file(ogg_44100) + # and finally update the JamTrackTrack with the new info + track["url_44"] = ogg_44100_s3_path + track["md5_44"] = ogg_44100_digest.hexdigest + track["length_44"] = File.new(ogg_44100).size + + track["url_48"] = ogg_48000_s3_path + track["md5_48"] = ::Digest::MD5.file(ogg_48000).hexdigest + track["length_48"] = File.new(ogg_48000).size + + # now create mp3 and aac files + mp3_48000 = File.join(tmp_dir, File.basename(basename, ".wav") + "-48000.mp3") + aac_48000 = File.join(tmp_dir, File.basename(basename, ".wav") + "-48000.aac") + + `ffmpeg -i "#{wav_file}" -ar 48000 -ab 192k "#{mp3_48000}"` + + `ffmpeg -i "#{wav_file}" -c:a libfdk_aac -b:a 192k "#{aac_48000}"` + + # upload the new ogg files to s3 + @@log.debug("uploading mp3 48000 to #{mp3_48000_s3_path}") + + jamkazam_s3_manager.upload(mp3_48000_s3_path, mp3_48000) + + @@log.debug("uploading aac 48000 to #{aac_48000_s3_path}") + + jamkazam_s3_manager.upload(aac_48000_s3_path, aac_48000) + + mp3_48000_digest = ::Digest::MD5.file(mp3_48000) + # and finally update the JamTrackTrack with the new info + track["url_mp3_48"] = mp3_48000_s3_path + track["md5_mp3_48"] = mp3_48000_digest.hexdigest + track["length_mp3_48"] = File.new(mp3_48000).size + + track["url_aac_48"] = aac_48000_s3_path + track["md5_aac_48"] = ::Digest::MD5.file(aac_48000).hexdigest + track["length_aac_48"] = File.new(aac_48000).size + + jam_track.save! + + # convert entire master ogg file to mp3, and push both to public destination + if track.track_type == 'Master' + preview_succeeded = synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_44100_digest) + + if !preview_succeeded + return false + end + elsif track.track_type == 'Track' || track.track_type == 'Click' + synchronize_track_preview(track, tmp_dir, ogg_44100) + end + + end + + track.save! + end + + def generate_mp3_aac_stem(jam_track, tmp_dir, skip_audio_upload) + jam_track.jam_track_tracks.each do |track| + + if track.original_audio_s3_path.nil? + + @@log.error("jam_track #{jam_track.name} has empty stem. stem: #{track.id}") + next + end + + puts "track.original_audio_s3_path #{track.original_audio_s3_path}" + basename = File.basename(track.original_audio_s3_path) + s3_dirname = File.dirname(track.original_audio_s3_path) + + # make a 44100 version, and a 48000 version + mp3_48000_filename = File.basename(basename, ".wav") + "-48000.mp3" + aac_48000_filename = File.basename(basename, ".wav") + "-48000.aac" + + mp3_48000_s3_path = track.filename(mp3_48000_filename) + aac_48000_s3_path = track.filename(aac_48000_filename) + + puts "mp3_48000_s3_path #{mp3_48000_s3_path}" + track.skip_uploader = true + + if skip_audio_upload + track["url_mp3_48"] = mp3_48000_filename + track["md5_mp3_48"] = 'md5' + track["length_mp3_48"] = 1 + + track["url_aac_48"] = aac_48000_filename + track["md5_aac_48"] = 'md5' + track["length_aac_48"] = 1 + + # we can't fake the preview as easily because we don't know the MD5 of the current item + #track["preview_md5"] = 'md5' + #track["preview_mp3_md5"] = 'md5' + #track["preview_url"] = track.preview_filename('md5', 'ogg') + #track["preview_length"] = 1 + #track["preview_mp3_url"] = track.preview_filename('md5', 'mp3') + #track["preview_mp3_length"] = 1 + #track["preview_start_time"] = 0 + else + wav_file = File.join(tmp_dir, basename) + + # the wave file might already be on the system... + + # don't bother with the same track twice + + next if track["url_mp3_48"] && track["url_aac_48"] + + # bring the original wav file down from S3 to local file system + JamTrackImporter::song_storage_manager.download(track.original_audio_s3_path, wav_file) unless File.exists?(wav_file) + + mp3_48000 = File.join(tmp_dir, File.basename(basename, ".wav") + "-48000.mp3") + aac_48000 = File.join(tmp_dir, File.basename(basename, ".wav") + "-48000.aac") + + `ffmpeg -i "#{wav_file}" -ar 48000 -ab 192k "#{mp3_48000}"` + + `ffmpeg -i "#{wav_file}" -c:a libfdk_aac -b:a 192k "#{aac_48000}"` + + # upload the new ogg files to s3 + @@log.debug("uploading mp3 48000 to #{mp3_48000_s3_path}") + + jamkazam_s3_manager.upload(mp3_48000_s3_path, mp3_48000) + + @@log.debug("uploading aac 48000 to #{aac_48000_s3_path}") + + jamkazam_s3_manager.upload(aac_48000_s3_path, aac_48000) + + mp3_48000_digest = ::Digest::MD5.file(mp3_48000) + # and finally update the JamTrackTrack with the new info + track["url_mp3_48"] = mp3_48000_s3_path + track["md5_mp3_48"] = mp3_48000_digest.hexdigest + track["length_mp3_48"] = File.new(mp3_48000).size + + track["url_aac_48"] = aac_48000_s3_path + track["md5_aac_48"] = ::Digest::MD5.file(aac_48000).hexdigest + track["length_aac_48"] = File.new(aac_48000).size + track.save + end + end + end + + def synchronize_duration(jam_track, ogg_44100) duration_command = "soxi -D \"#{ogg_44100}\"" output = `#{duration_command}` @@ -1155,58 +2006,15 @@ module JamRuby def synchronize_track_preview(track, tmp_dir, ogg_44100) - out_wav = File.join(tmp_dir, 'stripped.wav') - - burp_gaps = ['0.3', '0.2', '0.1', '0.05'] - - total_time_command = "soxi -D \"#{ogg_44100}\"" - total_time = `#{total_time_command}`.to_f - - result_code = -20 - stripped_time = total_time # default to the case where we just start the preview at the beginning - - burp_gaps.each do |gap| - command_strip_lead_silence = "sox \"#{ogg_44100}\" \"#{out_wav}\" silence 1 #{gap} 1%" - - @@log.debug("stripping silence: " + command_strip_lead_silence) - - output = `#{command_strip_lead_silence}` - - result_code = $?.to_i - - if result_code == 0 - stripped_time_command = "soxi -D \"#{out_wav}\"" - stripped_time_test = `#{stripped_time_command}`.to_f - - if stripped_time_test < 1 # meaning a very short duration - @@log.warn("could not determine the start of non-silencea. assuming beginning") - stripped_time = total_time # default to the case where we just start the preview at the beginning - else - stripped_time = stripped_time_test # accept the measured time of the stripped file and move on by using break - break - end - else - @@log.warn("unable to determine silence for jam_track #{track.original_filename}, #{output}") - stripped_time = total_time # default to the case where we just start the preview at the beginning - end - - end - - preview_start_time = total_time - stripped_time + preview_start_time = determine_start_time(ogg_44100, tmp_dir, track.original_filename) # this is in seconds; convert to integer milliseconds preview_start_time = (preview_start_time * 1000).to_i - preview_start_time = nil if preview_start_time < 0 + preview_start_time = 0 if preview_start_time < 0 track.preview_start_time = preview_start_time - if track.preview_start_time - @@log.debug("determined track start time to be #{track.preview_start_time}") - else - @@log.debug("determined track start time to be #{track.preview_start_time}") - end - track.process_preview(ogg_44100, tmp_dir) if track.preview_start_time if track.preview_generate_error @@ -1215,6 +2023,53 @@ module JamRuby end + def synchronize_aac_preview(track, tmp_dir, ogg_44100, ogg_digest) + begin + aac_44100 = File.join(tmp_dir, 'output-preview-44100.aac') + convert_aac_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{ogg_44100}\" -c:a libfdk_aac -b:a 192k \"#{aac_44100}\"" + @@log.debug("converting to aac using: " + convert_aac_cmd) + + convert_output = `#{convert_aac_cmd}` + + aac_digest = ::Digest::MD5.file(aac_44100) + + track["preview_aac_md5"] = aac_md5 = aac_digest.hexdigest + + # upload 44100 aac to public location + @@log.debug("uploading aac preview to #{track.preview_filename('aac')}") + public_jamkazam_s3_manager.upload(track.preview_filename(aac_digest.hexdigest, 'aac'), aac_44100, content_type: 'audio/aac', content_md5: aac_digest.base64digest) + + + track.skip_uploader = true + + original_aac_preview_url = track["preview_aac_url"] + + # and finally update the JamTrackTrack with the new info + track["preview_aac_url"] = track.preview_filename(aac_md5, 'aac') + track["preview_aac_length"] = File.new(aac_44100).size + track["preview_start_time"] = 0 + + if !track.save + finish("save_master_preview", track.errors.to_s) + return false + end + + # if all that worked, now delete old previews, if present + begin + public_jamkazam_s3_manager.delete(original_aac_preview_url) if original_aac_preview_url && original_aac_preview_url != track["preview_aac_url"] + rescue + puts "UNABLE TO CLEANUP OLD PREVIEW URL" + end + rescue Exception => e + finish("sync_master_preview_exception", e.to_s) + return false + end + + + return true + + end + def synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_digest) begin @@ -1226,20 +2081,33 @@ module JamRuby mp3_digest = ::Digest::MD5.file(mp3_44100) + aac_44100 = File.join(tmp_dir, 'output-preview-44100.aac') + convert_aac_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{ogg_44100}\" -c:a libfdk_aac -b:a 192k \"#{aac_44100}\"" + @@log.debug("converting to aac using: " + convert_aac_cmd) + + convert_output = `#{convert_aac_cmd}` + + aac_digest = ::Digest::MD5.file(aac_44100) + + track["preview_md5"] = ogg_md5 = ogg_digest.hexdigest track["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest + track["preview_aac_md5"] = aac_md5 = aac_digest.hexdigest - # upload 44100 ogg and mp3 to public location as well + # upload 44100 ogg, mp3, aac to public location as well @@log.debug("uploading ogg preview to #{track.preview_filename('ogg')}") public_jamkazam_s3_manager.upload(track.preview_filename(ogg_digest.hexdigest, 'ogg'), ogg_44100, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest) @@log.debug("uploading mp3 preview to #{track.preview_filename('mp3')}") public_jamkazam_s3_manager.upload(track.preview_filename(mp3_digest.hexdigest, 'mp3'), mp3_44100, content_type: 'audio/mpeg', content_md5: mp3_digest.base64digest) + @@log.debug("uploading aac preview to #{track.preview_filename('aac')}") + public_jamkazam_s3_manager.upload(track.preview_filename(aac_digest.hexdigest, 'aac'), aac_44100, content_type: 'audio/aac', content_md5: aac_digest.base64digest) track.skip_uploader = true original_ogg_preview_url = track["preview_url"] original_mp3_preview_url = track["preview_mp3_url"] + original_aac_preview_url = track["preview_aac_url"] # and finally update the JamTrackTrack with the new info track["preview_url"] = track.preview_filename(ogg_md5, 'ogg') @@ -1247,6 +2115,8 @@ module JamRuby # and finally update the JamTrackTrack with the new info track["preview_mp3_url"] = track.preview_filename(mp3_md5, 'mp3') track["preview_mp3_length"] = File.new(mp3_44100).size + track["preview_aac_url"] = track.preview_filename(aac_md5, 'mp3') + track["preview_aac_length"] = File.new(aac_44100).size track["preview_start_time"] = 0 if !track.save @@ -1258,6 +2128,7 @@ module JamRuby begin public_jamkazam_s3_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != track["preview_url"] public_jamkazam_s3_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"] + public_jamkazam_s3_manager.delete(original_aac_preview_url) if original_aac_preview_url && original_aac_preview_url != track["preview_aac_url"] rescue puts "UNABLE TO CLEANUP OLD PREVIEW URL" end @@ -1293,6 +2164,15 @@ module JamRuby original_artist = parsed_metalocation[1] name = parsed_metalocation[2] + if is_paris_storage? + bpm = parsed_metalocation[-1] + bpm.downcase! + if bpm.end_with?('bpm') + bpm = bpm[0..-4].to_f + end + metadata[:bpm] = bpm + end + success = synchronize_metadata(jam_track, metadata, metalocation, original_artist, name, options) return unless success @@ -1331,7 +2211,10 @@ module JamRuby attr_accessor :storage_format attr_accessor :tency_mapping attr_accessor :tency_metadata + attr_accessor :paris_mapping + attr_accessor :paris_metadata attr_accessor :summaries + attr_accessor :marks_approved def report_summaries @@log.debug("SUMMARIES DUMP") @@ -1357,19 +2240,43 @@ module JamRuby def song_storage_manager if is_tency_storage? tency_s3_manager + elsif is_paris_storage? + paris_s3_manager + elsif is_tim_tracks_storage? + tim_tracks_s3_manager + elsif is_drumma_storage? + drumma_s3_manager + elsif is_helbing_storage? + helbing_s3_manager else s3_manager end end def summaries - @summaries ||= {unknown_filetype: 0, no_instrument: 0, no_part: 0, total_tracks: 0, no_instrument_detail: {}, no_precount_num: 0, no_precount_detail: [], unique_artists: SortedSet.new} + @summaries ||= {unknown_filetype: 0, no_instrument: 0, no_part: 0, total_tracks: 0, no_instrument_detail: {}, no_precount_num: 0, no_precount_detail: [], unique_artists: SortedSet.new, multiple_masters: 0, total: 0} + end + + def drumma_s3_manager + @drumma_s3_manager ||= S3Manager.new('jamkazam-drumma', APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) end def tency_s3_manager @tency_s3_manager ||= S3Manager.new('jamkazam-tency', APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) end + def paris_s3_manager + @paris_s3_manager ||= S3Manager.new('jamkazam-paris', APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + end + + def tim_tracks_s3_manager + @tim_tracks_s3_manager ||= S3Manager.new('jamkazam-timtracks', APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + end + + def helbing_s3_manager + @tim_tracks_s3_manager ||= S3Manager.new('jamkazam-helbing', APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + end + def s3_manager @s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket_jamtracks, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) end @@ -1378,6 +2285,25 @@ module JamRuby @private_s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) end + def extract_paris_song_id(metalocation) + + first_path = metalocation.index('/') + return nil unless first_path + metalocation = metalocation[(first_path + 1)..-1] + + suffix = '/meta.yml' + metalocation = metalocation[0...-suffix.length] + + first_dash = metalocation.index('-') + return nil if first_dash.nil? + + id = metalocation[0...first_dash].strip + + return nil unless id.start_with?('S') # all start with S + return nil if id[1..-1].to_i == 0 # and number after that + id + end + def extract_tency_song_id(metalocation) # metalocation = mapped/4 Non Blondes - What's Up - 6475/meta.yml @@ -1398,15 +2324,80 @@ module JamRuby id end + def is_default_storage? + assert_storage_set + @storage_format == 'default' + end + + def is_drumma_storage? + assert_storage_set + @storage_format == 'Drumma' + end + def is_tency_storage? assert_storage_set @storage_format == 'Tency' end + def is_paris_storage? + assert_storage_set + @storage_format == 'Paris' + end + + def is_tim_tracks_storage? + assert_storage_set + @storage_format == 'TimTracks' + end + + def is_helbing_storage? + assert_storage_set + @storage_format == 'Helbing' + end + + def assert_storage_set raise "no storage_format set" if @storage_format.nil? end + def iterate_tim_tracks_song_storage(&blk) + count = 0 + song_storage_manager.list_directories('mapped').each do |song| + @@log.debug("searching through song directory '#{song}'") + + metalocation = "#{song}meta.yml" + + metadata = load_metalocation(metalocation) + + blk.call(metadata, metalocation) + + count += 1 + #break if count > 100 + + end + end + + def iterate_paris_song_storage(&blk) + count = 0 + song_storage_manager.list_directories('mapped').each do |song| + @@log.debug("searching through song directory '#{song}'") + + #next if song != 'mapped/S1555-Ashlee Simpson-L-O-V-E-96bpm/' + + metalocation = "#{song}meta.yml" + + metadata = load_metalocation(metalocation) + + if metadata.nil? + # we don't do a paris song unless it has metadata + next + end + blk.call(metadata, metalocation) + + count += 1 + #break if count > 1000 + end + end + def iterate_tency_song_storage(&blk) count = 0 song_storage_manager.list_directories('mapped').each do |song| @@ -1441,11 +2432,56 @@ module JamRuby end end + def iterate_drumma_song_storage(&blk) + song_storage_manager.list_directories.each do |song| + @@log.debug("searching through song directory '#{song}'") + + metalocation = "#{song}meta.yml" + + metadata = load_metalocation(metalocation) + + blk.call(metadata, metalocation) + + end + end + + def iterate_helbing_song_storage(&blk) + count = 0 + song_storage_manager.list_directories('mapped').each do |song| + @@log.debug("searching through song directory '#{song}'") + + metalocation = "#{song}meta.yml" + + metadata = load_metalocation(metalocation) + + blk.call(metadata, metalocation) + + count += 1 + #break if count > 100 + end + end + def iterate_song_storage(&blk) if is_tency_storage? iterate_tency_song_storage do |metadata, metalocation| blk.call(metadata, metalocation) end + elsif is_paris_storage? + iterate_paris_song_storage do |metadata, metalocation| + blk.call(metadata, metalocation) + end + elsif is_tim_tracks_storage? + iterate_tim_tracks_song_storage do |metadata, metalocation| + blk.call(metadata, metalocation) + end + elsif is_drumma_storage? + iterate_drumma_song_storage do |metadata, metalocation| + blk.call(metadata, metalocation) + end + elsif is_helbing_storage? + iterate_helbing_song_storage do |metadata, metalocation| + blk.call(metadata, metalocation) + end else iterate_default_song_storage do |metadata, metalocation| blk.call(metadata, metalocation) @@ -1456,6 +2492,9 @@ module JamRuby def dry_run iterate_song_storage do |metadata, metalocation| jam_track_importer = JamTrackImporter.new(@storage_format) + jam_track_importer.metadata = metadata + + JamTrackImporter.summaries[:total] += 1 jam_track_importer.dry_run(metadata, metalocation) end @@ -1477,6 +2516,7 @@ module JamRuby iterate_song_storage do |metadata, metalocation| importer = JamTrackImporter.new(@storage_format) + importer.metadata = metadata song_id = JamTrackImporter.extract_tency_song_id(metalocation) parsed_metalocation = importer.parse_metalocation(metalocation) @@ -1496,13 +2536,13 @@ module JamRuby CSV.open("only_in_s3.csv", "wb") do |csv| only_in_s3.each do |song_id| - csv << [ song_id, in_s3[song_id][:artist], in_s3[song_id][:song] ] + csv << [song_id, in_s3[song_id][:artist], in_s3[song_id][:song]] end end CSV.open("only_in_2k_selection.csv", "wb") do |csv| only_in_mapping.each do |song_id| - csv << [ song_id, in_mapping[song_id][:artist], in_mapping[song_id][:song] ] + csv << [song_id, in_mapping[song_id][:artist], in_mapping[song_id][:song]] end end @@ -1515,11 +2555,12 @@ module JamRuby break end end + def create_masters iterate_song_storage do |metadata, metalocation| next if metadata.nil? jam_track_importer = JamTrackImporter.new(@storage_format) - + jam_track_importer.metadata = metadata jam_track_importer.create_master(metadata, metalocation) end end @@ -1547,6 +2588,7 @@ module JamRuby metadata = load_metalocation(metalocation) jam_track_importer = JamTrackImporter.new + jam_track_importer.metadata = metadata jam_track_importer.dry_run(metadata, metalocation) end @@ -1581,6 +2623,66 @@ module JamRuby importer end + # hunts for the most recent .aac, .mp3, or .ogg file + def synchronize_preview_dev(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + + importer.synchronize_preview_dev(jam_track) + + importer.finish('success', nil) + importer + end + + def synchronize_jamtrack_aac_preview(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + + track = jam_track.master_track + + if track + Dir.mktmpdir do |tmp_dir| + ogg_44100 = File.join(tmp_dir, 'input.ogg') + private_s3_manager.download(track.url_by_sample_rate(44), ogg_44100) + ogg_44100_digest = ::Digest::MD5.file(ogg_44100) + if importer.synchronize_aac_preview(track, tmp_dir, ogg_44100, ogg_44100_digest) + importer.finish("success", nil) + end + end + else + importer.finish('no_master_track', nil) + end + importer + end + + # created for helbing tracks which had .mp3 as click track + def convert_click_track_to_wav(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + + track = jam_track.click_track + + if !track + Dir.mktmpdir do |tmp_dir| + + # something like: "mapped/Victor Young - Stella By Starlight/meta.yml" + metalocation = jam_track.metalocation + base_dir = metalocation[0...metalocation.rindex('/')] + click_mp3 = File.join(tmp_dir, 'Click.mp3') + click_wav = File.join(tmp_dir, 'Click.wav') + song_storage_manager.download(base_dir + '/Click.mp3', click_mp3) + + `ffmpeg -i "#{click_mp3}" "#{click_wav}"` + + song_storage_manager.upload(base_dir + '/Click.wav', click_wav) + importer.finish("success", nil) + end + else + importer.finish('success', nil) + end + importer + end + def synchronize_jamtrack_master_preview(jam_track) importer = JamTrackImporter.new importer.name = jam_track.name @@ -1603,6 +2705,104 @@ module JamRuby importer end + def synchronize_previews_dev + importers = [] + + JamTrack.all.each do |jam_track| + importers << synchronize_preview_dev(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 import_click_track(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + importer.import_click_track(jam_track) + + importer + end + + def generate_jmep(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + importer.generate_jmep(jam_track) + + importer + end + + def import_click_tracks + importers = [] + + licensor = JamTrackLicensor.find_by_name!('Stockton Helbing') + JamTrack.where(licensor_id: licensor.id).each do |jam_track| + #jam_track = JamTrack.find('126') + importers << import_click_track(jam_track) + end + + @@log.info("SUMMARY") + @@log.info("-------") + importers.each do |importer| + if importer + if importer.reason == "success" + @@log.info("#{importer.name} #{importer.reason}") + else + @@log.error("#{importer.name} failed to generate jmep.") + @@log.error("#{importer.name} reason=#{importer.reason}") + @@log.error("#{importer.name} detail=#{importer.detail}") + end + else + @@log.error("NULL IMPORTER") + end + + end + end + + def generate_jmeps + importers = [] + if is_tency_storage? + licensor = JamTrackLicensor.find_by_name!('Tency Music') + elsif is_paris_storage? + licensor = JamTrackLicensor.find_by_name!('Paris Music') + end + + JamTrack.where(licensor_id: licensor).each do |jam_track| + importers << generate_jmep(jam_track) + end + + @@log.info("SUMMARY") + @@log.info("-------") + importers.each do |importer| + if importer + if importer.reason == "success" + @@log.info("#{importer.name} #{importer.reason}") + else + @@log.error("#{importer.name} failed to generate jmep.") + @@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_previews importers = [] @@ -1629,6 +2829,57 @@ module JamRuby end end + def convert_click_track_to_wavs + importers = [] + + licensor = JamTrackLicensor.find_by_name!('Stockton Helbing') + JamTrack.where(licensor_id: licensor.id).each do |jam_track| + importers << convert_click_track_to_wav(jam_track) + end + + @@log.info("SUMMARY") + @@log.info("-------") + importers.each do |importer| + if importer + if importer.reason == "success" || importer.reason == "jam_track_exists" || importer.reason == "other_processing" + @@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_aac_previews + + importers = [] + + JamTrack.all.each do |jam_track| + importers << synchronize_jamtrack_aac_preview(jam_track) + end + + @@log.info("SUMMARY") + @@log.info("-------") + importers.each do |importer| + if importer + if importer.reason == "success" || importer.reason == "jam_track_exists" || importer.reason == "other_processing" + @@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 = [] @@ -1716,6 +2967,20 @@ module JamRuby importer end + def generate_mp3_aac_stem(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + + Dir.mktmpdir do |tmp_dir| + + audio_path = jam_track.metalocation[0...-"/meta.yml".length] + importer.associate_tracks_with_original_stems(jam_track, audio_path) + importer.generate_mp3_aac_stem(jam_track, tmp_dir, false) + end + + importer + end + def download_masters importers = [] @@ -1745,6 +3010,70 @@ module JamRuby filename.tr('/&@:,$=+?;\^`><{}[]#%~|', '') end + def generate_mp3_aac_stems(format) + importers = [] + + jam_tracks = [] + + tency = JamTrackLicensor.find_by_name('Tency Music') + + @@log.info("processing storage #{@storage_format}") + if is_tency_storage? + tency = JamTrackLicensor.find_by_name!('Tency Music') + jam_tracks = JamTrack.where(licensor_id: tency.id) + elsif is_paris_storage? + paris = JamTrackLicensor.find_by_name!('Paris Music') + jam_tracks = JamTrack.where(licensor_id: paris.id) + elsif is_default_storage? + # XXX IF WE ADD ANOTHER STORAGE, UPDATE THE WHERE TO EXCLUDE IT AS WELL + jam_tracks = JamTrack.where('licensor_id is null OR licensor_id != ?', tency.id) + else + raise 'unknown storage format!' + end + + jam_tracks.each do |jam_track| + + if ENV['NODE_COUNT'] + node_count = ENV['NODE_COUNT'].to_i + node_number = ENV['NODE_NUMBER'].to_i + raise "NO NODE_COUNT" if node_count == 0 + + jam_track_id = jam_track.id.to_i + jam_track_id = jam_track_id + node_number + if jam_track_id == 0 + @@log.warn("skipping #{jam_track_id} because non-numeric ID") + next + elsif jam_track_id % node_count == 0 + @@log.warn("starting JamTrack #{jam_track.id} (#{jam_track_id})") + importers << generate_mp3_aac_stem(jam_track) + else + @@log.warn("skipping #{jam_track_id}") + next + end + else + importers << generate_mp3_aac_stem(jam_track) + end + + end + + @@log.info("SUMMARY") + @@log.info("-------") + importers.each do |importer| + if importer + if importer.reason == "success" + @@log.info("#{importer.name} #{importer.reason}") + else + @@log.error("#{importer.name} failed to download.") + @@log.error("#{importer.name} reason=#{importer.reason}") + @@log.error("#{importer.name} detail=#{importer.detail}") + end + else + @@log.error("NULL IMPORTER") + end + + end + end + def generate_slugs JamTrack.all.each do |jam_track| jam_track.generate_slug @@ -1764,7 +3093,7 @@ module JamRuby count = 0 iterate_song_storage do |metadata, metalocation| - next if metadata.nil? && is_tency_storage? + next if metadata.nil? && (is_tency_storage? || is_paris_storage?) importer = synchronize_from_meta(metalocation, options) importers << importer @@ -1812,7 +3141,135 @@ module JamRuby end end - def genre_dump + def paris_genre_dump + load_paris_mappings + + genres = {} + @paris_metadata.each do |id, value| + genre1 = value[:genre1] + genre2 = value[:genre2] + genre3 = value[:genre3] + + genres[genre1.downcase.strip] = genre1.downcase.strip if genre1 + genres[genre2.downcase.strip] = genre2.downcase.strip if genre2 + genres[genre3.downcase.strip] = genre3.downcase.strip if genre3 + end + + all_genres = Genre.select(:id).all.map(&:id) + + all_genres = Set.new(all_genres) + genres.each do |genre, value| + found = all_genres.include? genre + + puts "#{genre}" unless found + end + end + + + def create_importer_from_existing(jam_track) + importer = JamTrackImporter.new(@storage_format) + importer.name = jam_track.name + importer.metadata = load_metalocation(jam_track.metalocation) + importer + end + + def resync_instruments(licensor) + + load_paris_mappings if @paris_mapping.nil? && is_paris_storage? + + JamTrack.where(licensor_id: licensor.id).each do |jam_track| + + if is_paris_storage? && @paris_metadata[jam_track.vendor_id].nil? + next + end + puts "RESYNCING JAMTRACK #{jam_track.id}" + JamTrackTrack.where(jam_track_id: jam_track.id).order(:position).each do |track| + puts "BEFORE TRACK #{track.instrument_id} #{track.part}" + end + + importer = create_importer_from_existing(jam_track) + importer.reassign_instrument_parts(jam_track) + + #puts ">>>>>>>>> HIT KEY TO CONTINUE <<<<<<<<<<" + #STDIN.gets + break + end + + end + + + def fix_artist_song_name (licensor) + + load_paris_mappings if @paris_mapping.nil? + + JamTrack.where(licensor_id: licensor.id).each do |jam_track| + + metadata = @paris_metadata[jam_track.vendor_id] + + if metadata.nil? + puts "OH NO! A Paris Song that does not belong! #{jam_track.id} #{jam_track.vendor_id}" + next + end + + puts "STARTING JAM_TRACK #{jam_track.id} #{jam_track.original_artist} #{jam_track.name}" + + jam_track.generate_slug + + if jam_track.changed? + puts "SLUG CHANGED! #{jam_track.changes.inspect}" + end + + if !jam_track.save + puts "dup slug!!!!: #{jam_track.id} #{jam_track.name} #{jam_track.original_artist}" + end + + if jam_track.changed? + jam_track.reload + end + + jam_track.original_artist = metadata[:original_artist] + jam_track.name = metadata[:name] + if jam_track.changed? + puts "ARTIST/NAME CHANGE: #{jam_track.changes.inspect}" + end + + if !jam_track.save + puts "unable to save new artist/song!" + end + end + end + + def fix_slugs(licensor) + + JamTrack.where(licensor_id: licensor.id).each do |jam_track| + + if jam_track.slug.end_with?('-') + puts "removing trailing dash" + jam_track.slug = jam_track.slug[0...-1] + if !jam_track.save + puts "dup slug!!!!: #{jam_track.id} #{jam_track.name} #{jam_track.original_artist}" + end + end + end + end + + def missing_masters(licensor) + + count = 0 + JamTrack.where(licensor_id: licensor.id).each do |jam_track| + + + if jam_track.master_track.nil? + puts "MISSING #{jam_track.metalocation}" + count += 1 + end + + end + + puts "missing master count: #{count}" + end + + def tency_genre_dump load_tency_mappings genres = {} @@ -1841,6 +3298,91 @@ module JamRuby end end + def load_marks_approved + @marks_approved = {} + Dir.mktmpdir do |tmp_dir| + mapping_file = 'marks_approved.csv' + mapping_csv = CSV.read(mapping_file, headers: true, return_headers: false) + + mapping_csv.each do |line| + approved = line[3] + track_url = line[2] + comments = line[7] + + approved.strip! if approved + track_url.strip! if track_url + comments.strip! if comments + + if approved == 'MJ' + prefix = 'https://www.jamkazam.com/landing/jamtracks/'.length + slug = track_url[prefix..-1] + + puts "MARKS APPROVED #{slug} #{comments}" + @marks_approved[slug] = comments + end + end + end + end + + def load_paris_mappings + Dir.mktmpdir do |tmp_dir| + mapping_file = File.join(tmp_dir, 'mapping.csv') + metadata_file = File.join(tmp_dir, 'metadata.csv') + + # this is a developer option to skip the download and look in the CWD to grab mapping.csv and metadata.csv + if ENV['PARIS_ALREADY_DOWNLOADED'] == '1' + mapping_file = 'paris_mapping.csv' + metadata_file = 'paris_metadata.csv' + else + paris_s3_manager.download('mapping/mapping.csv', mapping_file) + paris_s3_manager.download('mapping/metadata.csv', metadata_file) + end + + mapping_csv = CSV.read(mapping_file) + metadata_csv = CSV.read(metadata_file, headers: true, return_headers: false) + + @paris_mapping = {} + @paris_metadata = {} + # convert both to hashes + mapping_csv.each do |line| + instrument = line[1] + instrument.strip! if instrument + + part = line[2] + part.strip! if part + @paris_mapping[line[0].strip.downcase] = {instrument: instrument, part: part} + end + + metadata_csv.each do |line| + paris_artist = line[2] + # Paris artist in metadata file is often all caps + artist = paris_artist.split(' ').collect do |item| + if item == 'DJ' + 'DJ' + else + item.titleize + end + end.join(' ') + @paris_metadata[line[1].strip] = {id: line[1].strip, original_artist: artist, name: line[3], genre1: line[4], genre2: line[5], genre3: line[6]} + end + + @paris_metadata.each do |id, value| + + genres = [] + + genre1 = value[:genre1] + genre2 = value[:genre2] + genre3 = value[:genre3] + + genres << genre1.downcase.strip if genre1 + genres << genre2.downcase.strip if genre2 + genres << genre3.downcase.strip if genre3 + + value[:genres] = genres + end + end + end + def load_tency_mappings Dir.mktmpdir do |tmp_dir| mapping_file = File.join(tmp_dir, 'mapping.csv') @@ -1880,11 +3422,11 @@ module JamRuby genre4 = value[:genre4] genre5 = value[:genre5] - genres << genre1.downcase.strip if genre1 - genres << genre2.downcase.strip if genre2 - genres << genre3.downcase.strip if genre3 - genres << genre4.downcase.strip if genre4 - genres << genre5.downcase.strip if genre5 + genres << genre1.downcase.strip if genre1 + genres << genre2.downcase.strip if genre2 + genres << genre3.downcase.strip if genre3 + genres << genre4.downcase.strip if genre4 + genres << genre5.downcase.strip if genre5 value[:genres] = genres end @@ -1912,10 +3454,45 @@ module JamRuby end return tency_data + elsif is_paris_storage? + load_paris_mappings if @paris_mapping.nil? + song_id = extract_paris_song_id(metalocation) + + if song_id.nil? + puts "missing_song_id #{metalocation}" + return nil + end + + paris_data = @paris_metadata[song_id] + + if paris_data.nil? + @@log.warn("missing paris metadata '#{song_id}'") + end + + return paris_data + elsif is_drumma_storage? + + data = {} + begin + data = drumma_s3_manager.read_all(metalocation) + rescue AWS::S3::Errors::NoSuchKey + return {} + end + meta = YAML.load(data) + meta[:genres] = ['r&b'] if !meta[:genres] + meta else begin data = s3_manager.read_all(metalocation) - return YAML.load(data) + meta = YAML.load(data) + + if is_tim_tracks_storage? + meta[:genres] = ['acapella'] + elsif is_helbing_storage? + meta[:genres] = ['jazz'] + end + + meta rescue AWS::S3::Errors::NoSuchKey return nil end @@ -1933,6 +3510,7 @@ module JamRuby def sync_from_metadata(jam_track, meta, metalocation, options) jam_track_importer = JamTrackImporter.new(@storage_format) + jam_track_importer.metadata = meta JamTrack.connection.execute('SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED') @@ -1958,7 +3536,7 @@ module JamRuby meta = load_metalocation(metalocation) - if meta.nil? && is_tency_storage? + if meta.nil? && is_paris_storage? raise "no tency song matching this metalocation #{metalocation}" end jam_track_importer = nil @@ -1982,3 +3560,4 @@ module JamRuby end end end + diff --git a/ruby/lib/jam_ruby/jam_tracks_manager.rb b/ruby/lib/jam_ruby/jam_tracks_manager.rb index 0c4cada60..2435ad8df 100644 --- a/ruby/lib/jam_ruby/jam_tracks_manager.rb +++ b/ruby/lib/jam_ruby/jam_tracks_manager.rb @@ -42,7 +42,7 @@ module JamRuby jam_file_opts="" jam_track.jam_track_tracks.each do |jam_track_track| - next if jam_track_track.track_type != "Track" # master mixes do not go into the JKZ + next if jam_track_track.track_type == "Master" # master mixes do not go into the JKZ # use the jam_track_track ID as the filename.ogg/.wav, because it's important metadata nm = jam_track_track.id + File.extname(jam_track_track.url_by_sample_rate(sample_rate)) @@ -53,7 +53,8 @@ module JamRuby 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}")}" + part = jam_track_track.track_type == 'Click' ? 'ClickTrack' : jam_track_track.part + jam_file_opts << " -i #{Shellwords.escape("#{track_filename}+#{part}")}" end #puts "LS + " + `ls -la '#{tmp_dir}'` diff --git a/ruby/lib/jam_ruby/lib/s3_manager.rb b/ruby/lib/jam_ruby/lib/s3_manager.rb index c5f78b512..3fb9977b4 100644 --- a/ruby/lib/jam_ruby/lib/s3_manager.rb +++ b/ruby/lib/jam_ruby/lib/s3_manager.rb @@ -112,7 +112,7 @@ module JamRuby tree.children.select(&:leaf?).collect(&:key) end - def list_directories(prefix) + def list_directories(prefix = nil) tree = s3_bucket.as_tree(prefix: prefix) tree.children.select(&:branch?).collect(&:prefix) end @@ -121,6 +121,10 @@ module JamRuby s3_bucket.objects[filename].exists? end + def object(filename) + s3_bucket.objects[filename] + end + def length(filename) s3_bucket.objects[filename].content_length end diff --git a/ruby/lib/jam_ruby/lib/stats.rb b/ruby/lib/jam_ruby/lib/stats.rb index 697965526..69020c5ce 100644 --- a/ruby/lib/jam_ruby/lib/stats.rb +++ b/ruby/lib/jam_ruby/lib/stats.rb @@ -1,7 +1,8 @@ -require 'influxdb' +#require 'influxdb' # monkey patch InfluxDB client to clear the queue when asked to stop +=begin module InfluxDB class Client def stop! @@ -10,6 +11,7 @@ module InfluxDB end end end +=end module InfluxDB class Worker @@ -46,7 +48,7 @@ module JamRuby def self.destroy! if @client - @client.queue.clear if @client.queue + #@client.queue.clear if @client.queue @client.stop! end end @@ -60,8 +62,10 @@ module JamRuby influxdb_port = options[:influxdb_port] influxdb_async = options[:influxdb_async].nil? ? true : options[:influxdb_async] + # WHEN WE TRY TO BRING INFLUX BACK, TAKE THIS OUT + influx_db_right_now = false - if influxdb_database && influxdb_database.length > 0 + if influx_db_right_now && influxdb_database && influxdb_database.length > 0 @client = InfluxDB::Client.new influxdb_database, username: influxdb_username, password: influxdb_password, @@ -82,8 +86,32 @@ module JamRuby return if self.ignore # doing any writes in a test environment cause annoying puts to occur if @client && data && data.length > 0 - data['host'] = @host - data['time'] = Time.now.to_i + if data.has_key?('values') || data.has_key?(:values) + @client.write_point(name, data) + data['timestamp'] = Time.now.to_i + + tags = data['tags'] + key = 'tags' if tags + tags ||= data[:tags] + key = :tags if key.nil? + tags ||= {} + key = :tags if key.nil? + + tags['host'] = @host + data[key] = tags + else + tags = {} + values = {} + for k,v in data + if v.is_a?(String) + tags[k] = v + else + values[k] = v + end + end + data = {tags: tags, values: values} + end + @client.write_point(name, data) end end diff --git a/ruby/lib/jam_ruby/lib/subscription_message.rb b/ruby/lib/jam_ruby/lib/subscription_message.rb index 6be9f7d16..02b98f3b9 100644 --- a/ruby/lib/jam_ruby/lib/subscription_message.rb +++ b/ruby/lib/jam_ruby/lib/subscription_message.rb @@ -14,15 +14,29 @@ module JamRuby end def self.mount_source_up_requested(mount) - Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_UP_REQUEST}.to_json ) + Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_UP_REQUEST}.to_json) end def self.mount_source_down_requested(mount) - Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_DOWN_REQUEST}.to_json ) + Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_DOWN_REQUEST}.to_json) 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, current_packaging_step: jam_track_right.current_packaging_step, packaging_steps: jam_track_right.packaging_steps}.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 + + def self.mixdown_signing_job_change(jam_track_mixdown_package) + Notification.send_subscription_message('mixdown', jam_track_mixdown_package.id.to_s, + {signing_state: jam_track_mixdown_package.signing_state, + current_packaging_step: jam_track_mixdown_package.current_packaging_step, + packaging_steps: jam_track_mixdown_package.packaging_steps}.to_json) + end + + def self.test + Notification.send_subscription_message('some_key', '1', {field1: 'field1', field2: 'field2'}.to_json) end end end diff --git a/ruby/lib/jam_ruby/lib/timezone.rb b/ruby/lib/jam_ruby/lib/timezone.rb new file mode 100644 index 000000000..05aeaaae5 --- /dev/null +++ b/ruby/lib/jam_ruby/lib/timezone.rb @@ -0,0 +1,20 @@ + +module TZInfo + class Timezone + def pretty_name + name = self.name + + if name == "America/Chicago" || name == "US/Central" + name = "US Central Time" + elsif name == "America/New_York" || name == "US/Eastern" + name = "US Eastern Time" + elsif name == "America/Los_Angeles" || name == "US/Pacific" + name = "US Pacific Time" + elsif name == "America/Arizona" || name == "US/Mountain" + name = "US Mountain Time" + end + + name + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index e8cf40b1b..87cd8525b 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -8,6 +8,7 @@ module JamRuby USER_TARGET_PREFIX = "user:" CLIENT_TARGET_PREFIX = "client:" ALL_NATIVE_CLIENTS = '__ALL_NATIVE_CLIENTS__' # special value used in place of a specific client ID + ALL_ACTIVE_CLIENTS = '__ALL_ACTIVE_CLIENTS__' # special value used in place of a specific client ID def initialize() @type_values = {} @@ -68,7 +69,7 @@ module JamRuby end # create a login ack (login was successful) - def login_ack(public_ip, client_id, token, heartbeat_interval, music_session_id, reconnected, user_id, connection_expire_time, client_update_data = nil) + def login_ack(public_ip, client_id, token, heartbeat_interval, music_session_id, reconnected, user_id, connection_expire_time, username, client_update_data = nil) client_update = Jampb::ClientUpdate.new( product: client_update_data[:product], version: client_update_data[:version], @@ -85,7 +86,8 @@ module JamRuby :reconnected => reconnected, :user_id => user_id, :connection_expire_time => connection_expire_time, - :client_update => client_update + :client_update => client_update, + :username => username ) Jampb::ClientMessage.new( @@ -95,6 +97,55 @@ module JamRuby ) end + # create a login ack (login was successful) + def logout_ack() + + logout_ack = Jampb::LogoutAck.new() + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::LOGOUT_ACK, + :route_to => CLIENT_TARGET, + :logout_ack => logout_ack + ) + end + + # create a login ack (login was successful) + def diagnostic(message) + + diagnostic = Jampb::Diagnostic.new(message: message) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::DIAGNOSTIC, + :route_to => CLIENT_TARGET, + :diagnostic => diagnostic + ) + end + + + # create a login ack (login was successful) + def connect_ack(public_ip, client_id,heartbeat_interval, connection_expire_time, client_update_data = nil) + client_update = Jampb::ClientUpdate.new( + product: client_update_data[:product], + version: client_update_data[:version], + uri: client_update_data[:uri], + size: client_update_data[:size] + ) if client_update_data + + connect_ack = Jampb::ConnectAck.new( + :public_ip => public_ip, + :client_id => client_id, + :heartbeat_interval => heartbeat_interval, + :connection_expire_time => connection_expire_time, + :client_update => client_update + ) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::CONNECT_ACK, + :route_to => CLIENT_TARGET, + :connect_ack => connect_ack + ) + end + def download_available download_available = Jampb::DownloadAvailable.new @@ -404,34 +455,38 @@ module JamRuby ) end - def session_join(session_id, photo_url, source_user_id, msg, track_changes_counter) + # we send session_join to both every one in the session, and also every client belonging to user X + def session_join(session_id, photo_url, source_user_id, msg, track_changes_counter, source_client_id, route_to = CLIENT_TARGET) join = Jampb::SessionJoin.new( :session_id => session_id, :photo_url => photo_url, :source_user_id => source_user_id, :msg => msg, - :track_changes_counter => track_changes_counter + :track_changes_counter => track_changes_counter, + :client_id => source_client_id ) Jampb::ClientMessage.new( :type => ClientMessage::Type::SESSION_JOIN, - :route_to => CLIENT_TARGET, + :route_to => route_to, :session_join => join ) end - def session_depart(session_id, photo_url, msg, recording_id, track_changes_counter) + def session_depart(session_id, photo_url, msg, recording_id, track_changes_counter, source_client_id, source_user_id, route_to = CLIENT_TARGET) left = Jampb::SessionDepart.new( :session_id => session_id, :photo_url => photo_url, :msg => msg, :recording_id => recording_id, - :track_changes_counter => track_changes_counter + :track_changes_counter => track_changes_counter, + :client_id => source_client_id, + :source_user_id => source_user_id ) Jampb::ClientMessage.new( :type => ClientMessage::Type::SESSION_DEPART, - :route_to => CLIENT_TARGET, + :route_to => route_to, :session_depart => left ) end @@ -469,21 +524,41 @@ module JamRuby ) end - def scheduled_session_invitation(receiver_id, session_id, photo_url, msg, session_name, session_date, notification_id, created_at) - scheduled_session_invitation = Jampb::ScheduledSessionInvitation.new( + def scheduled_jamclass_invitation(receiver_id, session_id, photo_url, msg, session_name, session_date, notification_id, created_at, lesson_session_id) + scheduled_jamclas_invitation = Jampb::ScheduledJamclassInvitation.new( :session_id => session_id, :photo_url => photo_url, :msg => msg, :session_name => session_name, :session_date => session_date, :notification_id => notification_id, - :created_at => created_at + :created_at => created_at, + lesson_session_id: lesson_session_id ) Jampb::ClientMessage.new( - :type => ClientMessage::Type::SCHEDULED_SESSION_INVITATION, + :type => ClientMessage::Type::SCHEDULED_JAMCLASS_INVITATION, :route_to => USER_TARGET_PREFIX + receiver_id, - :scheduled_session_invitation => scheduled_session_invitation + :scheduled_jamclass_invitation => scheduled_jamclas_invitation + ) + end + + + def scheduled_session_invitation(receiver_id, session_id, photo_url, msg, session_name, session_date, notification_id, created_at) + scheduled_session_invitation = Jampb::ScheduledSessionInvitation.new( + :session_id => session_id, + :photo_url => photo_url, + :msg => msg, + :session_name => session_name, + :session_date => session_date, + :notification_id => notification_id, + :created_at => created_at + ) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::SCHEDULED_SESSION_INVITATION, + :route_to => USER_TARGET_PREFIX + receiver_id, + :scheduled_session_invitation => scheduled_session_invitation ) end @@ -736,6 +811,30 @@ module JamRuby ) end + def mixdown_sign_complete(receiver_id, mixdown_package_id) + signed = Jampb::MixdownSignComplete.new( + :mixdown_package_id => mixdown_package_id + ) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::MIXDOWN_SIGN_COMPLETE, + :route_to => USER_TARGET_PREFIX + receiver_id, #:route_to => CLIENT_TARGET, + :mixdown_sign_complete => signed + ) + end + + def mixdown_sign_failed(receiver_id, mixdown_package_id) + signed = Jampb::MixdownSignFailed.new( + :mixdown_package_id => mixdown_package_id + ) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::MIXDOWN_SIGN_FAILED, + :route_to => USER_TARGET_PREFIX + receiver_id, #:route_to => CLIENT_TARGET, + :mixdown_sign_failed=> signed + ) + end + def recording_master_mix_complete(receiver_id, recording_id, claimed_recording_id, band_id, msg, notification_id, created_at) recording_master_mix_complete = Jampb::RecordingMasterMixComplete.new( @@ -870,23 +969,71 @@ module JamRuby ) end - # creates the session chat message - def chat_message(session_id, sender_name, sender_id, msg, msg_id, created_at) + # creates the general purpose text message + def lesson_message(receiver_id, sender_photo_url, sender_name, sender_id, msg, notification_id, music_session_id, created_at, student_directed, purpose, lesson_session_id) + lesson_message = Jampb::LessonMessage.new( + :photo_url => sender_photo_url, + :sender_name => sender_name, + :sender_id => sender_id, + :receiver_id => receiver_id, + :msg => msg, + :notification_id => notification_id, + :music_session_id => music_session_id, + :created_at => created_at, + :student_directed => student_directed, + :purpose => purpose, + :lesson_session_id => lesson_session_id + ) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::LESSON_MESSAGE, + :route_to => USER_TARGET_PREFIX + receiver_id, + :lesson_message => lesson_message + ) + end + + # creates the chat message + def chat_message(session_id, sender_name, sender_id, msg, msg_id, created_at, channel, lesson_session_id, purpose, + attachment_id, attachment_type, attachment_name) chat_message = Jampb::ChatMessage.new( :sender_id => sender_id, :sender_name => sender_name, :msg => msg, :msg_id => msg_id, - :created_at => created_at + :created_at => created_at, + :channel => channel, + :lesson_session_id => lesson_session_id, + :purpose => purpose, + :attachment_id => attachment_id, + :attachment_type => attachment_type, + :attachment_name => attachment_name ) + if session_id + route_to = SESSION_TARGET_PREFIX + session_id + else + route_to = ALL_ACTIVE_CLIENTS + end + Jampb::ClientMessage.new( :type => ClientMessage::Type::CHAT_MESSAGE, - :route_to => SESSION_TARGET_PREFIX + session_id, + :route_to => route_to, :chat_message => chat_message ) end + def pair_attempt(jbid, scid, vtoken) + pair_attempt = Jampb::PairAttempt.new( + :scid => scid, + :vtoken => vtoken + ) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::PAIR_ATTEMPT, + :route_to => CLIENT_TARGET_PREFIX + jbid, + :pair_attempt => pair_attempt + ) + end # create a musician fresh session message def musician_session_fresh(session_id, user_id, username, photo_url) fresh = Jampb::MusicianSessionFresh.new( diff --git a/ruby/lib/jam_ruby/models/active_music_session.rb b/ruby/lib/jam_ruby/models/active_music_session.rb index 24917e5b7..dd809d35f 100644 --- a/ruby/lib/jam_ruby/models/active_music_session.rb +++ b/ruby/lib/jam_ruby/models/active_music_session.rb @@ -513,6 +513,7 @@ module JamRuby active_music_session.with_lock do # VRFS-1297 active_music_session.tick_track_changes + # VRFS-3986 connection = ConnectionManager.new.join_music_session(user, client_id, active_music_session, as_musician, tracks, audio_latency, video_sources) if connection.errors.any? @@ -578,6 +579,8 @@ module JamRuby user.update_progression_field(:first_music_session_at) MusicSessionUserHistory.save(active_music_session.id, user.id, client_id, tracks) + Notification.send_session_join(active_music_session, connection, user) + # only send this notification if it's a band session unless music_session.band.nil? Notification.send_band_session_join(music_session, music_session.band) @@ -764,8 +767,8 @@ module JamRuby feed.active = true feed.save - GoogleAnalyticsEvent.track_session_duration(self) - GoogleAnalyticsEvent.track_band_real_session(self) + #GoogleAnalyticsEvent.track_session_duration(self) + #GoogleAnalyticsEvent.track_band_real_session(self) end def open_jam_track(user, jam_track) @@ -774,6 +777,8 @@ module JamRuby self.opening_jam_track = true self.save self.opening_jam_track = false + + JamTrackSession.create_session(jam_track, user, self.music_session) if jam_track && user #self.tick_track_changes end @@ -838,5 +843,9 @@ module JamRuby stats end + + def lesson_session + music_session.lesson_session + end end end diff --git a/ruby/lib/jam_ruby/models/affiliate_distribution.rb b/ruby/lib/jam_ruby/models/affiliate_distribution.rb new file mode 100644 index 000000000..68988748e --- /dev/null +++ b/ruby/lib/jam_ruby/models/affiliate_distribution.rb @@ -0,0 +1,19 @@ +module JamRuby + class AffiliateDistribution < ActiveRecord::Base + + + belongs_to :sale_line_item, class_name: 'JamRuby::SaleLineItem' + belongs_to :affiliate_referral, class_name: 'JamRuby::AffiliatePartner', foreign_key: :affiliate_referral_id + + validates :affiliate_referral, presence:true + validates :affiliate_referral_fee_in_cents, numericality: {only_integer: false} + + def self.create(affiliate_referral, fee_in_cents, sale_line_item) + distribution = AffiliateDistribution.new + distribution.affiliate_referral = affiliate_referral + distribution.affiliate_referral_fee_in_cents = fee_in_cents + distribution.sale_line_item = sale_line_item + distribution + end + end +end diff --git a/ruby/lib/jam_ruby/models/affiliate_partner.rb b/ruby/lib/jam_ruby/models/affiliate_partner.rb index 934c16e77..a6dc437c7 100644 --- a/ruby/lib/jam_ruby/models/affiliate_partner.rb +++ b/ruby/lib/jam_ruby/models/affiliate_partner.rb @@ -2,6 +2,7 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base self.table_name = 'affiliate_partners' belongs_to :partner_user, :class_name => "JamRuby::User", :foreign_key => :partner_user_id, inverse_of: :affiliate_partner + has_one :school, class_name: "JamRuby::School" 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 @@ -9,7 +10,8 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base 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 + has_many :affiliate_distributions, :class_name => "JamRuby::AffiliateDistribution", foreign_key: :affiliate_referral_id + attr_accessible :partner_name, :partner_code, :partner_user_id, :entity_type, :rate, as: :admin ENTITY_TYPES = %w{ Individual Sole\ Proprietor Limited\ Liability\ Company\ (LLC) Partnership Trust/Estate S\ Corporation C\ Corporation Other } @@ -50,10 +52,18 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base record.entity_type ||= ENTITY_TYPES.first end + def display_name + partner_name || (partner_user ? partner_user.name : 'abandoned') + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/affiliates/#{id}" + end + # used by admin def self.create_with_params(params={}) raise 'not supported' - oo = self.new + oo = AffiliatePartner.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 @@ -65,15 +75,25 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base # used by web def self.create_with_web_params(user, params={}) - oo = self.new + oo = AffiliatePartner.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 + signed_legalese oo.save oo end + def self.create_from_school(school) + oo = AffiliatePartner.new + oo.partner_name = "Affiliate from School #{school.id}" + oo.partner_user = school.owner + oo.entity_type = 'Other' + oo.school = school + oo.signed_at = nil + oo.save + end + def self.coded_id(code=nil) self.where(:partner_code => code).limit(1).pluck(:id).first if code.present? end @@ -110,18 +130,22 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base 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} + def should_attribute_sale?(shopping_cart, user_to_check, instance) + + if created_within_affiliate_window(user_to_check, Time.now) + product_info = shopping_cart.product_info(instance) + # 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 + + if shopping_cart.is_lesson? + applicable_rate = lesson_rate else - false + applicable_rate = rate end + + {fee_in_cents: (product_info[:price] * 100 * real_quantity * applicable_rate.to_f).round} else - raise 'shopping cart type not implemented yet' + false end end @@ -227,23 +251,23 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base def self.sale_items_subquery(start_date, end_date, table_name) %{ - FROM sale_line_items + FROM affiliate_distributions inner join sale_line_items ON affiliate_distributions.sale_line_item_id = sale_line_items.id WHERE - (DATE(sale_line_items.created_at) >= DATE('#{start_date}') AND DATE(sale_line_items.created_at) <= DATE('#{end_date}')) + (DATE(affiliate_distributions.created_at) >= DATE('#{start_date}') AND DATE(affiliate_distributions.created_at) <= DATE('#{end_date}')) AND - sale_line_items.affiliate_referral_id = #{table_name}.affiliate_partner_id + affiliate_distributions.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 + FROM affiliate_distributions inner join sale_line_items ON affiliate_distributions.sale_line_item_id = sale_line_items.id WHERE - (DATE(sale_line_items.affiliate_refunded_at) >= DATE('#{start_date}') AND DATE(sale_line_items.affiliate_refunded_at) <= DATE('#{end_date}')) + (DATE(affiliate_distributions.affiliate_refunded_at) >= DATE('#{start_date}') AND DATE(affiliate_distributions.affiliate_refunded_at) <= DATE('#{end_date}')) AND - sale_line_items.affiliate_referral_id = #{table_name}.affiliate_partner_id + affiliate_distributions.affiliate_referral_id = #{table_name}.affiliate_partner_id AND - sale_line_items.affiliate_refunded = TRUE + affiliate_distributions.affiliate_refunded = TRUE } end # total up quarters by looking in sale_line_items for items that are marked as having a affiliate_referral_id @@ -263,22 +287,22 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base 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) + (SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND affiliate_distributions.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) + (SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND affiliate_distributions.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) + (SELECT SUM(affiliate_distributions.affiliate_referral_fee_in_cents) #{sale_items_subquery(start_date, end_date, 'affiliate_monthly_payments')} ), 0) + COALESCE( - (SELECT -SUM(affiliate_referral_fee_in_cents) + (SELECT -SUM(affiliate_distributions.affiliate_referral_fee_in_cents) #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_monthly_payments')} ), 0) @@ -316,22 +340,22 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base 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) + (SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND affiliate_distributions.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) + (SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND affiliate_distributions.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) + (SELECT SUM(affiliate_distributions.affiliate_referral_fee_in_cents) #{sale_items_subquery(start_date, end_date, 'affiliate_quarterly_payments')} ), 0) + COALESCE( - (SELECT -SUM(affiliate_referral_fee_in_cents) + (SELECT -SUM(affiliate_distributions.affiliate_referral_fee_in_cents) #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_quarterly_payments')} ), 0) @@ -348,7 +372,7 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base UPDATE affiliate_quarterly_payments SET closed = TRUE, closed_at = NOW() - WHERE year < #{year} OR quarter < #{quarter} + WHERE year < #{year} OR (year = #{year} AND quarter < #{quarter}) } ActiveRecord::Base.connection.execute(sql) @@ -469,4 +493,8 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base def affiliate_query_params AffiliatePartner::AFFILIATE_PARAMS + self.id.to_s end + + def to_s + display_name + end end diff --git a/ruby/lib/jam_ruby/models/affiliate_payment_charge.rb b/ruby/lib/jam_ruby/models/affiliate_payment_charge.rb new file mode 100644 index 000000000..376578b9d --- /dev/null +++ b/ruby/lib/jam_ruby/models/affiliate_payment_charge.rb @@ -0,0 +1,51 @@ +module JamRuby + class AffiliatePaymentCharge < Charge + + #has_one :teacher_payment, class_name: "JamRuby::TeacherPayment", foreign_key: :affiliate_charge_id + + def distribution + @distribution ||= teacher_payment.teacher_distribution + end + + def max_retries + 9999999 + end + + def teacher + @teacher ||= teacher_payment.teacher + end + + def charged_user + teacher + end + + def do_charge + + # source will let you supply a token. But... how to get a token in this case? + + stripe_charge = Stripe::Charge.create( + :amount => amount_in_cents, + :currency => "usd", + :customer => APP_CONFIG.stripe[:source_customer], + :description => construct_description, + :destination => teacher.teacher.stripe_account_id, + :application_fee => fee_in_cents, + ) + + stripe_charge + end + + def do_send_notices + #UserMailer.teacher_distribution_done(teacher_payment) + end + + def do_send_unable_charge + #UserMailer.teacher_distribution_fail(teacher_payment) + end + + def construct_description + #teacher_payment.teacher_distribution.description + end + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/anonymous_user.rb b/ruby/lib/jam_ruby/models/anonymous_user.rb index 24bc2b104..55c788e90 100644 --- a/ruby/lib/jam_ruby/models/anonymous_user.rb +++ b/ruby/lib/jam_ruby/models/anonymous_user.rb @@ -15,20 +15,47 @@ module JamRuby ShoppingCart.where(anonymous_user_id: @id).order('created_at DESC') end + def destroy_all_shopping_carts ShoppingCart.destroy_all(anonymous_user_id: @id) end + def destroy_jam_track_shopping_carts + ShoppingCart.destroy_all(anonymous_user_id: @id, cart_type: JamTrack::PRODUCT_TYPE) + end + def admin false end def has_redeemable_jamtrack + raise "not a cookied anonymous user" if @cookies.nil? + APP_CONFIG.one_free_jamtrack_per_user && !@cookies[:redeemed_jamtrack] end + def gifted_jamtracks + 0 + end + + def free_jamtracks + if has_redeemable_jamtrack + 1 + else + 0 + end + end + + def show_free_jamtrack? + ShoppingCart.user_has_redeemable_jam_track?(self) + end + def signup_hint SignupHint.where(anonymous_user_id: @id).where('expires_at > ?', Time.now).first end + + def reload + + end end end diff --git a/ruby/lib/jam_ruby/models/artifact_update.rb b/ruby/lib/jam_ruby/models/artifact_update.rb index 9b671df91..ae6f66596 100644 --- a/ruby/lib/jam_ruby/models/artifact_update.rb +++ b/ruby/lib/jam_ruby/models/artifact_update.rb @@ -4,7 +4,7 @@ module JamRuby DEFAULT_ENVIRONMENT = 'public' CLIENT_PREFIX = 'JamClient' - PRODUCTS = ["#{CLIENT_PREFIX}/Win32", "#{CLIENT_PREFIX}/MacOSX"] + PRODUCTS = ["#{CLIENT_PREFIX}/Win32", "#{CLIENT_PREFIX}/MacOSX", "#{CLIENT_PREFIX}/JamBlaster", "#{CLIENT_PREFIX}/JamBlasterClient"] self.primary_key = 'id' attr_accessible :version, :uri, :sha1, :environment, :product, as: :admin diff --git a/ruby/lib/jam_ruby/models/auto_emailer.rb b/ruby/lib/jam_ruby/models/auto_emailer.rb new file mode 100644 index 000000000..415971666 --- /dev/null +++ b/ruby/lib/jam_ruby/models/auto_emailer.rb @@ -0,0 +1,12 @@ +module JamRuby + class AutoEmailer + + belongs_to :user, class_name: "JamRuby::User" + + def hourly_job + AutoEmailer.where('mailed = false').where("email_at < ", Time.now).each do |auto_emailer| + AutoMailer.public_send(auto_emailer.name.to_sym, auto_emailer) + end + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/band_search.rb b/ruby/lib/jam_ruby/models/band_search.rb index 5ac5c6d06..c03ae2507 100644 --- a/ruby/lib/jam_ruby/models/band_search.rb +++ b/ruby/lib/jam_ruby/models/band_search.rb @@ -252,7 +252,6 @@ module JamRuby def _process_results_page(_results) - puts "PROCESS RESULT SPAGE" @results = _results if user @user_counters = @results.inject({}) { |hh,val| hh[val.id] = {}; hh } diff --git a/ruby/lib/jam_ruby/models/base_search.rb b/ruby/lib/jam_ruby/models/base_search.rb index ed2feefb7..685133be8 100644 --- a/ruby/lib/jam_ruby/models/base_search.rb +++ b/ruby/lib/jam_ruby/models/base_search.rb @@ -102,11 +102,19 @@ module JamRuby def self.search_target_class end + def self.genre_ids + @@genre_ids ||= Hash[ *Genre.pluck(:id).collect { |v| [ v, v ] }.flatten ] + end + + def self.instrument_ids + @@instrument_ids ||= Hash[ *Instrument.pluck(:id).collect { |v| [ v, v ] }.flatten ] + end + def _genres(rel, query_data=json) gids = query_data[KEY_GENRES] unless gids.blank? - allgids = Genre.order(:id).pluck(:id) - gids = gids.select { |gg| allgids.index(gg).present? } + allgids = self.class.genre_ids + gids = gids.select { |gg| allgids.has_key?(gg) } unless gids.blank? gidsql = gids.join("','") @@ -119,8 +127,8 @@ module JamRuby def _instruments(rel, query_data=json) unless (instruments = query_data[KEY_INSTRUMENTS]).blank? - instrids = Instrument.order(:id).pluck(:id) - instruments = instruments.select { |ii| instrids.index(ii['instrument_id']).present? } + instrids = self.class.instrument_ids + instruments = instruments.select { |ii| instrids.has_key?(ii['instrument_id']) } unless instruments.blank? instsql = "SELECT player_id FROM musicians_instruments WHERE ((" diff --git a/ruby/lib/jam_ruby/models/campaign_spend.rb b/ruby/lib/jam_ruby/models/campaign_spend.rb new file mode 100644 index 000000000..a34fae2c4 --- /dev/null +++ b/ruby/lib/jam_ruby/models/campaign_spend.rb @@ -0,0 +1,5 @@ +module JamRuby + class CampaignSpend < ActiveRecord::Base + + end +end diff --git a/ruby/lib/jam_ruby/models/charge.rb b/ruby/lib/jam_ruby/models/charge.rb new file mode 100644 index 000000000..83d9400ce --- /dev/null +++ b/ruby/lib/jam_ruby/models/charge.rb @@ -0,0 +1,137 @@ +module JamRuby + class Charge < ActiveRecord::Base + + belongs_to :user, class_name: "JamRuby::User" + + validates :sent_billing_notices, inclusion: {in: [true, false]} + + def max_retries + raise "not implemented" + end + def do_charge(force) + raise "not implemented" + end + def do_send_notices + raise "not implemented" + end + def do_send_unable_charge + raise "not implemented" + end + def charge_retry_hours + 24 + end + def charged_user + raise "not implemented" + end + + def charge(force = false) + + stripe_charge = nil + + if !self.billed + + # check if we can bill at the moment + if !force && last_billing_attempt_at && (charge_retry_hours.hours.ago < last_billing_attempt_at) + return false + end + + if !force && !billing_should_retry + return false + end + + + # bill the user right now. if it fails, move on; will be tried again + self.billing_attempts = self.billing_attempts + 1 + self.billing_should_retry = self.billing_attempts < max_retries + self.last_billing_attempt_at = Time.now + self.save(validate: false) + + begin + + stripe_charge = do_charge(force) + self.stripe_charge_id = stripe_charge.id + self.billed = true + self.billed_at = Time.now + self.save(validate: false) + rescue Stripe::StripeError => e + + stripe_handler(e) + + subject = "Unable to charge user #{charged_user.email} for lesson #{self.id} (stripe)" + body = "user=#{charged_user.email}\n\nbilling_error_reason=#{billing_error_reason}\n\nbilling_error_detail = #{billing_error_detail}" + AdminMailer.alerts({subject: subject, body: body}) + do_send_unable_charge + + return false + rescue Exception => e + subject = "Unable to charge user #{charged_user.email} for lesson #{self.id} (unhandled)" + body = "user=#{charged_user.email}\n\nbilling_error_reason=#{billing_error_reason}\n\nbilling_error_detail = #{billing_error_detail}" + AdminMailer.alerts({subject: subject, body: body}) + unhandled_handler(e) + return false + end + + end + + if !self.sent_billing_notices + # If the charge is successful, then we post the charge to the student’s payment history, + # and associate the charge with the lesson, so that everyone knows the student has paid, and we send an email + + do_send_notices + + self.sent_billing_notices = true + self.sent_billing_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(validate: false) + end + + return stripe_charge + end + + def unhandled_handler(e, reason = 'unhandled_exception') + self.billing_error_reason = reason + if e.cause + self.billing_error_detail = e.cause.to_s + "\n" + e.cause.backtrace.join("\n\t") if e.cause.backtrace + self.billing_error_detail << "\n\n" + self.billing_error_detail << e.to_s + "\n" + e.backtrace.join("\n\t") if e.backtrace + else + self.billing_error_detail = e.to_s + "\n" + e.backtrace.join("\n\t") if e.backtrace + end + puts "Charge: unhandled exception #{billing_error_reason}, #{billing_error_detail}" + self.save(validate: false) + end + + def is_card_declined? + billed == false && billing_error_reason == 'card_declined' + end + + def is_card_expired? + billed == false && billing_error_reason == 'card_expired' + end + + def last_billed_at_date + last_billing_attempt_at.strftime("%B %d, %Y") if last_billing_attempt_at + end + def stripe_handler(e) + + msg = e.to_s + + if msg.include?('declined') + self.billing_error_reason = 'card_declined' + self.billing_error_detail = msg + elsif msg.include?('expired') + self.billing_error_reason = 'card_expired' + self.billing_error_detail = msg + elsif msg.include?('processing') + self.billing_error_reason = 'processing_error' + self.billing_error_detail = msg + else + self.billing_error_reason = 'stripe' + self.billing_error_detail = msg + end + + self.save(validate: false) + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/chat_message.rb b/ruby/lib/jam_ruby/models/chat_message.rb index 2d0e704ac..aad2a4b0b 100644 --- a/ruby/lib/jam_ruby/models/chat_message.rb +++ b/ruby/lib/jam_ruby/models/chat_message.rb @@ -3,18 +3,71 @@ module JamRuby include HtmlSanitize html_sanitize strict: [:message] + CHANNEL_LESSON = 'lesson' self.table_name = 'chat_messages' self.primary_key = 'id' default_scope { order('created_at DESC') } + attr_accessor :ignore_message_checks + attr_accessible :user_id, :message, :music_session_id belongs_to :user belongs_to :music_session + belongs_to :target_user, class_name: "JamRuby::User" + belongs_to :lesson_session, class_name: "JamRuby::LessonSession" + belongs_to :music_notation, class_name: "JamRuby::MusicNotation" + belongs_to :claimed_recording, class_name: "JamRuby::ClaimedRecording" validates :user, presence: true - validates :message, length: {minimum: 1, maximum: 255}, no_profanity: true + validates :message, length: {minimum: 1, maximum: 255}, no_profanity: true, unless: :ignore_message_checks + + def self.create(user, music_session, message, channel, client_id, target_user = nil, lesson_session = nil, purpose = nil, music_notation = nil, recording = nil) + source_user = user + # we hide the real source user; always make it from the teacher + if lesson_session && user.id != lesson_session.student_id + source_user = lesson_session.teacher + end + chat_msg = ChatMessage.new + chat_msg.user_id = source_user.id + chat_msg.music_session_id = music_session.id if music_session + chat_msg.message = message + chat_msg.channel = channel + chat_msg.target_user = target_user + chat_msg.lesson_session = lesson_session + chat_msg.purpose = purpose + chat_msg.music_notation = music_notation + chat_msg.claimed_recording = recording + + + if lesson_session + chat_msg.ignore_message_checks = true + + if source_user.id == lesson_session.student.id + lesson_session.teacher_unread_messages = true + target = lesson_session.teacher + Notification.send_lesson_message('chat', lesson_session, false, message) + else + lesson_session.student_unread_messages = true + target = lesson_session.student + Notification.send_lesson_message('chat', lesson_session, true, message) + end + + lesson_session.save(validate: false) + + # a nil purpose means 'normal chat', which is the only time we should send an email + if !target.online? && purpose.nil? && message.present? + UserMailer.lesson_chat(chat_msg).deliver! + + end + end + + if chat_msg.save + ChatMessage.send_chat_msg music_session, chat_msg, source_user, client_id, channel, lesson_session, purpose, target_user, music_notation, recording + end + chat_msg + end class << self @@ -29,10 +82,19 @@ module JamRuby start = params[:start].presence start = start.to_i || 0 - music_session_id = params[:music_session] + query = ChatMessage.where('channel = ?', params[:channel]) - query = ChatMessage.where('music_session_id = ?', music_session_id) - .offset(start).limit(limit) + if params.has_key? (:music_session) + music_session_id = params[:music_session] + query = ChatMessage.where('music_session_id = ?', music_session_id) + end + + if params.has_key? (:lesson_session) + lesson_session_id = params[:lesson_session] + query = ChatMessage.where('lesson_session_id = ?', lesson_session_id) + end + + query = query.offset(start).limit(limit).order('created_at DESC').includes([:user]) if query.length == 0 [query, nil] @@ -43,20 +105,46 @@ module JamRuby end end - def send_chat_msg(music_session, chat_msg, user, client_id) + def send_chat_msg(music_session, chat_msg, user, client_id, channel, lesson_session, purpose, target_user, music_notation, claimed_recording) + music_session_id = music_session.id if music_session + lesson_session_id = lesson_session.id if lesson_session + + if music_notation + attachment_id = music_notation.id + attachment_type = music_notation.attachment_type + attachment_name = music_notation.file_name + elsif claimed_recording + attachment_id = claimed_recording.id + attachment_type = 'recording' + attachment_name = claimed_recording.name + end + + msg = @@message_factory.chat_message( - music_session.id, + music_session_id, user.name, user.id, chat_msg.message, chat_msg.id, - chat_msg.created_at.utc.iso8601 + chat_msg.created_at.utc.iso8601, + channel, + lesson_session_id, + purpose, + attachment_id, + attachment_type, + attachment_name ) - @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id}) + if channel == 'session' + @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id}) + elsif channel == 'global' + @@mq_router.publish_to_active_clients(msg) + elsif channel == 'lesson' + @@mq_router.publish_to_user(target_user.id, msg, sender = {:client_id => client_id}) + @@mq_router.publish_to_user(user.id, msg, sender = {:client_id => client_id}) + end + end - end - end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/connection.rb b/ruby/lib/jam_ruby/models/connection.rb index 14c4813f6..18f250616 100644 --- a/ruby/lib/jam_ruby/models/connection.rb +++ b/ruby/lib/jam_ruby/models/connection.rb @@ -22,6 +22,7 @@ module JamRuby has_many :tracks, :class_name => "JamRuby::Track", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all has_many :backing_tracks, :class_name => "JamRuby::BackingTrack", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all has_many :video_sources, :class_name => "JamRuby::VideoSource", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all + has_one :jamblaster, class_name: "JamRuby::Jamblaster", foreign_key: "client_id" validates :metronome_open, :inclusion => {:in => [true, false]} validates :as_musician, :inclusion => {:in => [true, false, nil]} @@ -89,6 +90,12 @@ module JamRuby joining_session end + def same_network_jamblasters + # return all jamblasters that are currently connected with the same public IP address (don't include this one though) + Jamblaster.joins(:connection).where("connections.ip_address = ?", ip_address).where("connections.id != ?", id).limit(100) + end + + def can_join_music_session # puts "can_join_music_session: #{music_session_id} was #{music_session_id_was}" if music_session_id_changed? @@ -111,13 +118,13 @@ module JamRuby if music_session.musician_access if music_session.approval_required - unless music_session.creator == user || music_session.invited_musicians.exists?(user.id) + if !(music_session.music_session.creator == user || music_session.creator == user || music_session.invited_musicians.exists?(user.id)) errors.add(:approval_required, ValidationMessages::INVITE_REQUIRED) return false end end else - unless music_session.creator == user || music_session.invited_musicians.exists?(user.id) + if !(music_session.music_session.creator == user || music_session.creator == user || music_session.invited_musicians.exists?(user.id)) errors.add(:musician_access, ValidationMessages::INVITE_REQUIRED) return false end @@ -164,7 +171,7 @@ module JamRuby self.connected? && self.as_musician? && 0 < (count = self.music_session.connected_participant_count) - GoogleAnalyticsEvent.report_session_participant(count) + #GoogleAnalyticsEvent.report_session_participant(count) end true end @@ -190,7 +197,7 @@ module JamRuby unless tracks.nil? tracks.each do |track| t = Track.new - t.instrument = Instrument.find(track["instrument_id"]) + t.instrument = Instrument.find_by_id(track["instrument_id"]) || Instrument.find('acoustic guitar') t.connection = self t.sound = track["sound"] t.client_track_id = track["client_track_id"] diff --git a/ruby/lib/jam_ruby/models/crash_dump.rb b/ruby/lib/jam_ruby/models/crash_dump.rb index 6c4e54d84..f71d8c21d 100644 --- a/ruby/lib/jam_ruby/models/crash_dump.rb +++ b/ruby/lib/jam_ruby/models/crash_dump.rb @@ -1,6 +1,8 @@ module JamRuby class CrashDump < ActiveRecord::Base + include JamRuby::S3ManagerMixin + self.table_name = "crash_dumps" self.primary_key = 'id' @@ -15,7 +17,7 @@ module JamRuby before_validation(:on => :create) do self.created_at ||= Time.now self.id = SecureRandom.uuid - self.uri = "dump/#{self.id}-#{self.created_at.to_i}" + self.uri = "dumps/#{created_at.strftime('%Y-%m-%d')}/#{self.id}.zip" end def user_email @@ -23,5 +25,8 @@ module JamRuby self.user.email end + def sign_url(expiration_time = 3600 * 24 * 7, secure=true) + s3_manager.sign_url(self[:ri], {:expires => expiration_time, :secure => secure}) + end end end diff --git a/ruby/lib/jam_ruby/models/download_tracker.rb b/ruby/lib/jam_ruby/models/download_tracker.rb new file mode 100644 index 000000000..ad0b955cb --- /dev/null +++ b/ruby/lib/jam_ruby/models/download_tracker.rb @@ -0,0 +1,148 @@ +module JamRuby + class DownloadTracker < ActiveRecord::Base + + @@log = Logging.logger[DownloadTracker] + + belongs_to :user, :class_name => "JamRuby::User" + belongs_to :mixdown, :class_name => "JamRuby::JamTrackMixdownPackage", foreign_key: 'mixdown_id' + belongs_to :stem, :class_name => "JamRuby::JamTrackTrack", foreign_key: 'stem_id' + belongs_to :jam_track, :class_name => "JamRuby::JamTrack" + + # one of mixdown or stem need to be specified. could validate this? + validates :user, presence:true + validates :remote_ip, presence: true + #validates :paid, presence: true + validates :jam_track, presence: :true + + def self.create(user, remote_ip, target, owned, fingerprint, is_client) + dt = DownloadTracker.new + dt.user = user + dt.remote_ip = remote_ip + dt.paid = owned + dt.is_client = is_client + dt.fingerprint = fingerprint + if target.is_a?(JamTrack) + dt.jam_track_id = target.id + elsif target.is_a?(JamTrackTrack) + dt.jam_track_id = target.jam_track_id + elsif target.is_a?(JamTrackMixdownPackage) + dt.jam_track_id = target.jam_track_mixdown.jam_track_id + end + if !dt.save + @@log.error("unable to create Download Tracker: #{dt.errors.inspect}") + end + dt + end + + def self.check(user, remote_ip, target, owned, fingerprint, is_client) + + return false unless APP_CONFIG.guard_against_browser_fraud + + return false if user.admin + + return false if UserWhitelist.listed(user) + + create(user, remote_ip, target, owned, fingerprint, is_client) + + # let's check the following + blacklisted = alert_freebies_snarfer(remote_ip, owned) + + alert_user_sharer(user) + + blacklisted + end + + # somebody who has shared account info with a large number of people + # high number of downloads of the same user from different IP addresses that were or were not paid for + # raw query created by this code: + # SELECT distinct(user_id), count(user_id) FROM "download_trackers" WHERE (created_at > NOW() - '30 days'::interval) GROUP BY user_id HAVING count(distinct(remote_ip)) >= 2 + def self.check_user_sharer(max, user_id = nil) + query = DownloadTracker.select('distinct(user_id), count(user_id)') + query = query.where("created_at > NOW() - '#{APP_CONFIG.download_tracker_day_range} days'::interval") + if !user_id.nil? + query = query.where('user_id = ?', user_id) + end + + query.group(:user_id).having("count(distinct(remote_ip)) >= #{max}") + end + + + + + # somebody who has figured out how to bypass cookie based method of identity checking, and is getting lots of free JamTracks + # high number of downloads of different jam tracks from different users for the same IP address that weren't paid for + # raw query created by this code: + # SELECT distinct(remote_ip), count(remote_ip) FROM "download_trackers" WHERE (paid = false) AND (created_at > NOW() - '30 days'::interval) GROUP BY remote_ip HAVING count(distinct(jam_track_id)) >= 2 + def self.check_freebie_snarfer(max, remote_ip = nil) + + query = DownloadTracker.select('distinct(remote_ip), count(remote_ip)').where("paid = false") + query = query.where("created_at > NOW() - '#{APP_CONFIG.download_tracker_day_range} days'::interval") + if !remote_ip.nil? + query = query.where('remote_ip = ?', remote_ip) + end + query.group(:remote_ip).having("count(distinct(jam_track_id)) >= #{max}") + end + + def self.alert_user_sharer(user) + violation = check_user_sharer(APP_CONFIG.max_user_ip_address, user.id).first + + if violation + body = "User has downloaded from too many IP addresses #{user.id}\n" + body << "Download Count: #{violation['count']}\n" + body << "User URL #{user.admin_url}\n" + body << "Add to blacklist: #{UserBlacklist.admin_url}" + + AdminMailer.alerts({ + subject:"Account IP Access Violation. USER: #{user.email}", + body:body + }).deliver_now + end + end + + def self.alert_freebies_snarfer(remote_ip, owned) + + if owned + return false + end + + if !IpWhitelist.listed(remote_ip) + violation = check_freebie_snarfer(APP_CONFIG.max_multiple_users_same_ip, remote_ip).first + end + + + if violation + body = "IP Address: #{remote_ip}\n" + body << "Download Count: #{violation['count']}\n" + body << "Add to blacklist: #{IpBlacklist.admin_url}" + body << "Check Activity: #{IpBlacklist.admin_activity_url(remote_ip)}" + + AdminMailer.alerts({ + subject:"Single IP Access Violation. IP:#{remote_ip}", + body:body + }).deliver_now + + # and now shut them down + if Rails.application.config.ban_jamtrack_downloaders + blacklist = IpBlacklist.new + blacklist.remote_ip = remote_ip + blacklist.notes = 'auto' + blacklist.save + return blacklist + end + end + return false + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/download_trackers/" + id + end + + def to_s + if stem? + "stem:#{stem} #{remote_ip} #{user}" + else + "mixdown:#{mixdown} #{remote_ip} #{user}" + end + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/email_batch_new_musician.rb b/ruby/lib/jam_ruby/models/email_batch_new_musician.rb index f9d616158..ada7997c2 100644 --- a/ruby/lib/jam_ruby/models/email_batch_new_musician.rb +++ b/ruby/lib/jam_ruby/models/email_batch_new_musician.rb @@ -118,7 +118,7 @@ SQL self.fetch_recipients do |user, new_musicians| self.opt_in_count += 1 bset = EmailBatchSet.new_musician_set(self, user, new_musicians) - UserMailer.new_musicians(user, new_musicians).deliver + UserMailer.new_musicians(user, new_musicians).deliver_now end self.sent_count = self.opt_in_count self.save diff --git a/ruby/lib/jam_ruby/models/email_blacklist.rb b/ruby/lib/jam_ruby/models/email_blacklist.rb new file mode 100644 index 000000000..ac4940224 --- /dev/null +++ b/ruby/lib/jam_ruby/models/email_blacklist.rb @@ -0,0 +1,30 @@ +module JamRuby + class EmailBlacklist < ActiveRecord::Base + + attr_accessible :email, :source, :notes, as: :admin + + @@log = Logging.logger[EmailBlacklist] + + validates :email, uniqueness: true + + def self.banned(user) + EmailBlacklist.count(:conditions => "email = '#{user.email.downcase}'") >= 1 + end + + def self.listed(user) + EmailBlacklist.count(:conditions => "email= '#{user.id}'") == 1 + end + + def self.admin_url + APP_CONFIG.admin_root_url + "/admin/email_blacklists/" + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/email_blacklists/" + id + end + + def to_s + user + end + end +end diff --git a/ruby/lib/jam_ruby/models/generic_state.rb b/ruby/lib/jam_ruby/models/generic_state.rb index 9d6a23904..5722a9424 100644 --- a/ruby/lib/jam_ruby/models/generic_state.rb +++ b/ruby/lib/jam_ruby/models/generic_state.rb @@ -27,6 +27,10 @@ module JamRuby GenericState.singleton.affiliate_tallied_at end + def self.bounce_check_at + GenericState.singleton.bounce_check_at + end + def self.singleton GenericState.find('default') end diff --git a/ruby/lib/jam_ruby/models/genre.rb b/ruby/lib/jam_ruby/models/genre.rb index 91d80f755..24a93a830 100644 --- a/ruby/lib/jam_ruby/models/genre.rb +++ b/ruby/lib/jam_ruby/models/genre.rb @@ -15,6 +15,10 @@ module JamRuby # genres has_and_belongs_to_many :recordings, :class_name => "JamRuby::Recording", :join_table => "recordings_genres" + # teachers + has_many :teachers, :class_name => "JamRuby::Teacher", :through => :teachers_genres + has_many :teachers_genres, :class_name => "JamRuby::TeacherGenre" + # jam tracks has_many :genres_jam_tracks, :class_name => "JamRuby::GenreJamTrack", :foreign_key => "genre_id" has_many :jam_tracks, :through => :genres_jam_tracks, :class_name => "JamRuby::JamTrack", :source => :genre @@ -22,5 +26,13 @@ module JamRuby def to_s description end + + def self.jam_track_list + sql = "SELECT DISTINCT genre_id FROM genres_jam_tracks WHERE genre_id IS NOT NULL" + Genre.select("DISTINCT(genres.id), genres.*") + .where("genres.id IN (#{sql})") + .order('genres.description ASC, genres.id') + end + end end diff --git a/ruby/lib/jam_ruby/models/genre_jam_track.rb b/ruby/lib/jam_ruby/models/genre_jam_track.rb index aa05e4fd8..933ef26bc 100644 --- a/ruby/lib/jam_ruby/models/genre_jam_track.rb +++ b/ruby/lib/jam_ruby/models/genre_jam_track.rb @@ -2,7 +2,10 @@ module JamRuby class GenreJamTrack < ActiveRecord::Base self.table_name = 'genres_jam_tracks' - belongs_to :jam_track, class_name: 'JamRuby::JamTrack' - belongs_to :genre, class_name: 'JamRuby::Genre' + + attr_accessible :jam_track_id, :genre_id + + belongs_to :jam_track, class_name: 'JamRuby::JamTrack', inverse_of: :genres_jam_tracks + belongs_to :genre, class_name: 'JamRuby::Genre', inverse_of: :genres_jam_tracks end end diff --git a/ruby/lib/jam_ruby/models/gift_card.rb b/ruby/lib/jam_ruby/models/gift_card.rb new file mode 100644 index 000000000..0dc5f94e8 --- /dev/null +++ b/ruby/lib/jam_ruby/models/gift_card.rb @@ -0,0 +1,36 @@ +# represents the gift card you hold in your hand +module JamRuby + class GiftCard < ActiveRecord::Base + + @@log = Logging.logger[GiftCard] + + JAM_TRACKS_5 = 'jam_tracks_5' + JAM_TRACKS_10 = 'jam_tracks_10' + CARD_TYPES = + [ + JAM_TRACKS_5, + JAM_TRACKS_10 + ] + + + belongs_to :user, class_name: "JamRuby::User" + + validates :card_type, presence: true, inclusion: {in: CARD_TYPES} + validates :code, presence: true, uniqueness: true + + after_save :check_gifted + + def check_gifted + if user && user_id_changed? + if card_type == JAM_TRACKS_5 + user.gifted_jamtracks += 5 + elsif card_type == JAM_TRACKS_10 + user.gifted_jamtracks += 10 + else + raise "unknown card type #{card_type}" + end + user.save! + end + end + end +end diff --git a/ruby/lib/jam_ruby/models/gift_card_purchase.rb b/ruby/lib/jam_ruby/models/gift_card_purchase.rb new file mode 100644 index 000000000..0cfb00807 --- /dev/null +++ b/ruby/lib/jam_ruby/models/gift_card_purchase.rb @@ -0,0 +1,17 @@ +# reperesents the gift card you buy from the site (but physical gift card is modeled by GiftCard) +module JamRuby + class GiftCardPurchase < ActiveRecord::Base + + @@log = Logging.logger[GiftCardPurchase] + + attr_accessible :user, :gift_card_type + + def name + gift_card_type.sale_display + end + + # who purchased the card? + belongs_to :user, class_name: "JamRuby::User" + belongs_to :gift_card_type, class_name: "JamRuby::GiftCardType" + end +end diff --git a/ruby/lib/jam_ruby/models/gift_card_type.rb b/ruby/lib/jam_ruby/models/gift_card_type.rb new file mode 100644 index 000000000..8294dbfed --- /dev/null +++ b/ruby/lib/jam_ruby/models/gift_card_type.rb @@ -0,0 +1,70 @@ +# reperesents the gift card you buy from the site (but physical gift card is modeled by GiftCard) +module JamRuby + class GiftCardType < ActiveRecord::Base + + @@log = Logging.logger[GiftCardType] + + PRODUCT_TYPE = 'GiftCardType' + + JAM_TRACKS_5 = 'jam_tracks_5' + JAM_TRACKS_10 = 'jam_tracks_10' + CARD_TYPES = + [ + JAM_TRACKS_5, + JAM_TRACKS_10 + ] + + validates :card_type, presence: true, inclusion: {in: CARD_TYPES} + + def self.jam_track_5 + GiftCardType.find(JAM_TRACKS_5) + end + + def self.jam_track_10 + GiftCardType.find(JAM_TRACKS_10) + end + + def name + sale_display + end + + def price + if card_type == JAM_TRACKS_5 + 10.00 + elsif card_type == JAM_TRACKS_10 + 20.00 + else + raise "unknown card type #{card_type}" + end + end + + + def sale_display + if card_type == JAM_TRACKS_5 + 'JamTracks Gift Card (5)' + elsif card_type == JAM_TRACKS_10 + 'JamTracks Gift Card (10)' + else + raise "unknown card type #{card_type}" + end + end + + def plan_code + if card_type == JAM_TRACKS_5 + "jamtrack-giftcard-5" + elsif card_type == JAM_TRACKS_10 + "jamtrack-giftcard-10" + else + raise "unknown card type #{card_type}" + end + end + + def sales_region + 'Worldwide' + end + + def to_s + sale_display + end + end +end diff --git a/ruby/lib/jam_ruby/models/instrument.rb b/ruby/lib/jam_ruby/models/instrument.rb index 1a3fa8df7..e8b407135 100644 --- a/ruby/lib/jam_ruby/models/instrument.rb +++ b/ruby/lib/jam_ruby/models/instrument.rb @@ -43,8 +43,18 @@ module JamRuby # music sessions has_and_belongs_to_many :music_sessions, :class_name => "JamRuby::ActiveMusicSession", :join_table => "genres_music_sessions" + # teachers + has_many :teachers, :class_name => "JamRuby::Teacher", through: :teachers_instruments + has_many :teachers_instruments, class_name: "JamRuby::TeacherInstrument" + def self.standard_list - return Instrument.where('instruments.popularity > 0').order('instruments.popularity DESC, instruments.description ASC') + return Instrument.where('instruments.popularity > 0').order('instruments.description ASC') + end + + def self.jam_track_list + sql = "SELECT DISTINCT instrument_id FROM jam_track_tracks WHERE instrument_id IS NOT NULL" + Instrument.where("instruments.id IN (#{sql})") + .order('instruments.description ASC') end def icon_name diff --git a/ruby/lib/jam_ruby/models/invitation.rb b/ruby/lib/jam_ruby/models/invitation.rb index 81dde690a..221ba5b21 100644 --- a/ruby/lib/jam_ruby/models/invitation.rb +++ b/ruby/lib/jam_ruby/models/invitation.rb @@ -1,6 +1,7 @@ module JamRuby class Invitation < ActiveRecord::Base + INVITATION_NOT_TEACHER_VALIDATION_ERROR = "Lessons can only sent invitations to teachers" FRIENDSHIP_REQUIRED_VALIDATION_ERROR = "You can only invite friends" MEMBERSHIP_REQUIRED_OF_MUSIC_SESSION = "You must be a member of the music session to send invitations on behalf of it" JOIN_REQUEST_IS_NOT_FOR_RECEIVER_AND_MUSIC_SESSION = "You can only associate a join request with an invitation if that join request comes from the invited user and if it's for the same music session" @@ -15,7 +16,7 @@ module JamRuby validates :receiver, :presence => true validates :music_session, :presence => true - validate :require_sender_in_music_session, :require_are_friends_or_requested_to_join + validate :require_sender_in_music_session, :require_are_friends_or_requested_to_join_or_teacher private @@ -25,15 +26,20 @@ module JamRuby end end - def require_are_friends_or_requested_to_join + def require_are_friends_or_requested_to_join_or_teacher if !join_request.nil? && (join_request.user != receiver || join_request.music_session != music_session) errors.add(:join_request, JOIN_REQUEST_IS_NOT_FOR_RECEIVER_AND_MUSIC_SESSION ) elsif join_request.nil? # we only check for friendship requirement if this was not in response to a join_request - unless receiver.friends.exists? sender.id - errors.add(:receiver, FRIENDSHIP_REQUIRED_VALIDATION_ERROR) + if !receiver.friends.exists?(sender.id) && (music_session.is_lesson? && receiver != music_session.lesson_session.teacher) + if !receiver.friends.exists?(sender.id) + errors.add(:receiver, FRIENDSHIP_REQUIRED_VALIDATION_ERROR) + elsif (music_session.is_lesson? && receiver != music_session.lesson_session.teacher) + errors.add(:receiver, INVITATION_NOT_TEACHER_VALIDATION_ERROR) + end end end + end end end diff --git a/ruby/lib/jam_ruby/models/ip_blacklist.rb b/ruby/lib/jam_ruby/models/ip_blacklist.rb new file mode 100644 index 000000000..2b4cd429c --- /dev/null +++ b/ruby/lib/jam_ruby/models/ip_blacklist.rb @@ -0,0 +1,34 @@ +module JamRuby + class IpBlacklist < ActiveRecord::Base + + attr_accessible :remote_ip, :notes, as: :admin + + @@log = Logging.logger[IpBlacklist] + + validates :remote_ip, presence: true, uniqueness: true + + def self.banned(remote_ip) + IpBlacklist.where("remote_ip = '#{remote_ip}' AND remote_ip not in (select remote_ip from ip_whitelists where remote_ip = '#{remote_ip}')").count == 1 + end + + def self.listed(remote_ip) + IpBlacklist.where("remote_ip = '#{remote_ip}'").count == 1 + end + + def self.admin_url + APP_CONFIG.admin_root_url + "/admin/ip_blacklists/" + end + + def self.admin_activity_url(remote_ip) + APP_CONFIG.admin_root_url + "/admin/download_trackers?q[remote_ip_equals]=#{URI.escape(remote_ip)}&commit=Filter&order=id_desc" + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/ip_blacklists/" + id + end + + def to_s + remote_ip + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/ip_whitelist.rb b/ruby/lib/jam_ruby/models/ip_whitelist.rb new file mode 100644 index 000000000..eef310c2b --- /dev/null +++ b/ruby/lib/jam_ruby/models/ip_whitelist.rb @@ -0,0 +1,30 @@ +module JamRuby + class IpWhitelist< ActiveRecord::Base + + attr_accessible :remote_ip, :notes, as: :admin + + @@log = Logging.logger[IpWhitelist] + + validates :remote_ip, presence:true, uniqueness:true + + def self.listed(remote_ip) + IpWhitelist.where("remote_ip = '#{remote_ip}'").count == 1 + end + + def self.admin_url + APP_CONFIG.admin_root_url + "/admin/ip_whitelists/" + end + + def self.admin_activity_url(remote_ip) + APP_CONFIG.admin_root_url + "/admin/download_trackers?q[remote_ip_equals]=#{URI.escape(remote_ip)}&commit=Filter&order=id_desc" + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/ip_whitelists/" + id + end + + def to_s + remote_ip + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/jam_class_report.rb b/ruby/lib/jam_ruby/models/jam_class_report.rb new file mode 100644 index 000000000..4ba2a6ce6 --- /dev/null +++ b/ruby/lib/jam_ruby/models/jam_class_report.rb @@ -0,0 +1,104 @@ +# CREATE FUNCTION jamclass_report RETURNS TABLE (campaign VARCHAR, spend numeric(8,2), registrations INTEGER, td_customers INTEGER, jamclass_rev NUMERIC(8,2), td4 INTEGER, td2 INTEGER, td1 INTEGER, spend_td NUMERIC(8,2), purchases_0 INTEGER, purchases_1 INTEGER, purchases_2 INTEGER, purchases_3 INTEGER, purchases_rest INTEGER) VOLATILE AS $$ + +module JamRuby + class JamClassReport < ActiveRecord::Base + + def self.update_spend + + end + def self.analyse(campaign_filter = nil) + + User.transaction do + user_purchase = "CREATE TEMPORARY TABLE user_jamclass_purchases (user_id VARCHAR(64) NOT NULL, purchases INTEGER DEFAULT 0) ON COMMIT DROP" + user_inserts = "INSERT INTO user_jamclass_purchases (user_id, purchases) (SELECT id, COUNT(user_jamclass_purchases.user_id) FROM users LEFT OUTER JOIN user_jamclass_purchases ON users.id = user_jamclass_purchases.user_id GROUP BY users.id)" + User.connection.execute(user_purchase) + User.connection.execute(user_inserts) + + jamclass_revenue = "(SELECT SUM(price) * 0.25 FROM lesson_package_purchases WHERE lesson_package_purchases.lesson_package_type_id = 'single') + (SELECT SUM(6) FROM lesson_package_purchases WHERE lesson_package_purchases.lesson_package_type_id = 'test-drive-1') + (SELECT SUM(10) FROM lesson_package_purchases WHERE lesson_package_purchases.lesson_package_type_id = 'test-drive-2') + (SELECT SUM(10) FROM lesson_package_purchases WHERE lesson_package_purchases.lesson_package_type_id = 'test-drive')" + td_users = "COUNT(td_purchases.id)" + td4 = "COUNT(td4_purchases.id)" + td2 = "COUNT(td2_purchases.id)" + td1 = "COUNT(td1_purchases.id)" + spend_td = "SELECT (CASE WHEN COUNT(td_purchases.id) = 0 THEN NULL ELSE avg(campaign_spends.spend) / COUNT(td_purchases.id) END)" + purchases0 = "COUNT(CASE WHEN user_jamclass_purchases.purchases = 0 THEN 1 ELSE NULL END) / COUNT(user_jamclass_purchases.purchases)" + purchases1 = "COUNT(CASE WHEN user_jamclass_purchases.purchases = 1 THEN 1 ELSE NULL END) / COUNT(user_jamclass_purchases.purchases)" + purchases2 = "COUNT(CASE WHEN user_jamclass_purchases.purchases = 2 THEN 1 ELSE NULL END) / COUNT(user_jamclass_purchases.purchases)" + purchases3 = "COUNT(CASE WHEN user_jamclass_purchases.purchases = 3 THEN 1 ELSE NULL END) / COUNT(user_jamclass_purchases.purchases)" + purchases_rest = "COUNT(CASE WHEN user_jamclass_purchases.purchases >= 3 THEN 1 ELSE NULL END) / COUNT(user_jamclass_purchases.purchases)" + purchases0_count = "COUNT(CASE WHEN user_jamclass_purchases.purchases = 0 THEN 1 ELSE NULL END)" + purchases1_count = "COUNT(CASE WHEN user_jamclass_purchases.purchases = 1 THEN 1 ELSE NULL END)" + purchases2_count = "COUNT(CASE WHEN user_jamclass_purchases.purchases = 2 THEN 1 ELSE NULL END)" + purchases3_count = "COUNT(CASE WHEN user_jamclass_purchases.purchases = 3 THEN 1 ELSE NULL END)" + purchases_rest_count = "COUNT(CASE WHEN user_jamclass_purchases.purchases >= 3 THEN 1 ELSE NULL END)" + purchases_count = "COUNT(user_jamclass_purchases.purchases)" + query = User.select("date_trunc( 'month', users.created_at ) as cohort, origin_utm_campaign AS campaign, avg(campaign_spends.spend) as spend, count(users.id) AS registrations, (#{td_users}) as td_customers, (#{jamclass_revenue}) as jamclass_rev, (#{td4}) AS td4, (#{td2}) AS td2, (#{td1}) AS td1, (#{spend_td}) as spend_td, (#{purchases0}) as purchases0, (#{purchases1}) as purchases1, (#{purchases2}) as purchases2, (#{purchases3}) as purchases3, (#{purchases_rest}) as purchases_rest, (#{purchases0_count}) as purchases0_count, (#{purchases1_count}) as purchases1_count, (#{purchases2_count}) as purchases2_count, (#{purchases3_count}) as purchases3_count, (#{purchases_rest_count}) as purchases_rest_count, (#{purchases_count}) as purchases_count") + .joins(%Q{ + LEFT OUTER JOIN + campaign_spends + ON + campaign_spends.month = date_part('month', users.created_at) AND year = date_part('year', users.created_at) AND campaign_spends.campaign = users.origin_utm_campaign + }) + .joins(%Q{ + LEFT OUTER JOIN + lesson_package_purchases + ON + lesson_package_purchases.user_id = users.id + }) + .joins(%Q{ + LEFT OUTER JOIN + lesson_package_purchases AS td4_purchases + ON + lesson_package_purchases.user_id = users.id AND lesson_package_purchases.id = 'test-drive' + }) + .joins(%Q{ + LEFT OUTER JOIN + lesson_package_purchases AS td2_purchases + ON + lesson_package_purchases.user_id = users.id AND lesson_package_purchases.id = 'test-drive-2' + }) + .joins(%Q{ + LEFT OUTER JOIN + lesson_package_purchases AS td1_purchases + ON + lesson_package_purchases.user_id = users.id AND lesson_package_purchases.id = 'test-drive-1' + }) + .joins(%Q{ + LEFT OUTER JOIN + lesson_package_purchases AS td_purchases + ON + lesson_package_purchases.user_id = users.id AND lesson_package_purchases.id in ('test-drive', 'test-drive-2', 'test-drive-1') + }) + .joins(%Q{ + INNER JOIN + user_jamclass_purchases AS user_jamclass_purchases + ON + user_jamclass_purchases.user_id = users.id + }) + .group('users.origin_utm_campaign, cohort') + + + user_inserts = "INSERT INTO jam_class_reports (cohort, campaign, spend, registrations, td_customers, jamclass_rev, td4, td2, td1, spend_td, purchases0, purchases1, purchases2, purchases3, purchases_rest, purchases0_count, purchases1_count, purchases2_count, purchases3_count, purchases_rest_count, purchases_count) (#{query.to_sql})" + User.connection.execute("DELETE FROM jam_class_reports") + User.connection.execute(user_inserts) + purchases0 = "SUM(jam_class_reports.purchases0_count) / SUM(jam_class_reports.purchases_count)" + purchases1 = "SUM(jam_class_reports.purchases1_count) / SUM(jam_class_reports.purchases_count)" + purchases2 = "SUM(jam_class_reports.purchases2_count) / SUM(jam_class_reports.purchases_count)" + purchases3 = "SUM(jam_class_reports.purchases3_count) / SUM(jam_class_reports.purchases_count)" + purchases_rest = "SUM(jam_class_reports.purchases_rest_count) / SUM(jam_class_reports.purchases_count)" + + group_inserts = "INSERT INTO jam_class_reports (cohort, campaign, spend, registrations, td_customers, jamclass_rev, td4, td2, td1, spend_td, purchases0, purchases1, purchases2, purchases3, purchases_rest) + (SELECT NULL, jam_class_reports.campaign, SUM(spend), SUM(registrations), SUM(td_customers), SUM(jamclass_rev), SUM(td4), SUM(td2), SUM(td1), CASE WHEN SUM(td4) + SUM (td2) + SUM(td1) = 0 THEN NULL ELSE (SUM(spend) / (SUM(td4) + SUM (td2) + SUM(td1))) END, + #{purchases0}, #{purchases1}, #{purchases2}, #{purchases3}, #{purchases_rest} FROM jam_class_reports + GROUP BY campaign)" + User.connection.execute(group_inserts) + reports = JamClassReport.order('campaign, cohort DESC NULLS LAST') + if campaign_filter + reports = reports.where(campaign: campaign_filter) + end + + reports + end + + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb index 7c9dacf01..6e07858a3 100644 --- a/ruby/lib/jam_ruby/models/jam_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- module JamRuby class JamTrack < ActiveRecord::Base include JamRuby::S3ManagerMixin @@ -18,7 +19,7 @@ module JamRuby :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, :genre_ids, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, :duration, - :server_fixation_date, :hfa_license_status, :hfa_license_desired, :alternative_license_status, :hfa_license_number, :hfa_song_code, :album_title, as: :admin + :server_fixation_date, :hfa_license_status, :hfa_license_desired, :alternative_license_status, :hfa_license_number, :hfa_song_code, :album_title, :year, as: :admin validates :name, presence: true, length: {maximum: 200} validates :plan_code, presence: true, uniqueness: true, length: {maximum: 50 } @@ -52,7 +53,7 @@ module JamRuby belongs_to :licensor , class_name: 'JamRuby::JamTrackLicensor', foreign_key: 'licensor_id', :inverse_of => :jam_tracks - has_many :genres_jam_tracks, :class_name => "JamRuby::GenreJamTrack", :foreign_key => "jam_track_id" + has_many :genres_jam_tracks, :class_name => "JamRuby::GenreJamTrack", :foreign_key => "jam_track_id", inverse_of: :jam_track has_many :genres, :through => :genres_jam_tracks, :class_name => "JamRuby::Genre", :source => :genre has_many :jam_track_tracks, -> { order('track_type ASC, position ASC, part ASC, instrument_id ASC' )},:class_name => "JamRuby::JamTrackTrack" @@ -72,6 +73,8 @@ module JamRuby # VRFS-2916 jam_tracks.id is varchar: ADD has_many :plays, :class_name => "JamRuby::PlayablePlay", :as => :playable, :dependent => :destroy + has_many :jam_track_session, :class_name => "JamRuby::JamTrackSession" + # when we know what JamTrack this refund is related to, these are associated belongs_to :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook' @@ -85,6 +88,10 @@ module JamRuby after_save :sync_reproduction_royalty after_save :sync_onboarding_exceptions + def increment_version! + self.version = version.to_i + 1 + save! + end def sync_reproduction_royalty @@ -154,6 +161,9 @@ module JamRuby true end + def sale_display + "JamTrack: " + name + end def duplicate_positions? counter = {} jam_track_tracks.each do |track| @@ -230,6 +240,8 @@ module JamRuby return {artists: [], songs: []} end + options[:show_purchased_only] = options[:show_purchased_only] + options[:limit] = options[:limit] || 5 options[:artist_search] = options[:match] @@ -243,6 +255,15 @@ module JamRuby {artists: artists, songs:songs} end + def purchase_stubs(user) + JamTrack. + select(['jam_tracks.id', :name, :original_artist, :year, 'jam_track_rights.created_at AS purchased_at']). + joins(:jam_track_rights). + where("jam_track_rights.user_id = ?", user.id). + includes(:genres). + order([:original_artist, :name]) + end + def index(options, user) if options[:page] page = options[:page].to_i @@ -253,12 +274,12 @@ module JamRuby limit = options[:limit] limit ||= 20 limit = limit.to_i + per_page = limit else limit = per_page end start = (page -1 )* per_page - limit = per_page else limit = options[:limit] limit ||= 20 @@ -302,7 +323,16 @@ module JamRuby end if options[:artist].present? - query = query.where("original_artist=?", options[:artist]) + artist_param = options[:artist] + # todo: add licensor option + if artist_param == 'Stockton Helbing' + licensor = JamTrackLicensor.find_by_name('Stockton Helbing') + if licensor + query = query.where(licensor_id: licensor.id) + end + else + query = query.where("original_artist=?", options[:artist]) + end end if options[:song].present? @@ -317,6 +347,7 @@ module JamRuby 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") query = query.group("original_artist") query = query.order('jam_tracks.original_artist') + query = query.includes([{ jam_track_tracks: :instrument }, { genres_jam_tracks: :genre }]) else query = query.group("jam_tracks.id") if options[:sort_by] == 'jamtrack' @@ -328,16 +359,27 @@ module JamRuby end - query = query.where("jam_tracks.status = ?", 'Production') unless user.admin + if (! user.try(:admin) && 'development' != Rails.env) + query = query.where("jam_tracks.status = ?", 'Production') + end unless options[:genre].blank? query = query.joins(:genres) query = query.where('genre_id = ? ', options[:genre]) end - 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_track_tracks.instrument_id = '#{options[:instrument]}' and jam_track_tracks.track_type = 'Track'") unless options[:instrument].blank? query = query.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank? + # FIXME: n+1 queries for rights and genres + # query = query.includes([{ jam_track_tracks: :instrument }, + # :jam_track_tap_ins, + # :jam_track_rights, + # :genres]) + # { genres_jam_tracks: :genre }, + # query = query.includes([{ jam_track_tracks: :instrument }, + # { genres_jam_tracks: :genre }]) + count = query.total_entries if count == 0 @@ -387,6 +429,11 @@ module JamRuby query = query.where("jam_tracks.status = ?", 'Production') unless user.admin + if options[:show_purchased_only] + query = query.joins(:jam_track_rights) + query = query.where("jam_track_rights.user_id = ?", user.id) + end + if options[:artist_search] tsquery = Search.create_tsquery(options[:artist_search]) if tsquery @@ -414,13 +461,43 @@ module JamRuby end end + def click_track_file + JamTrackFile.where(jam_track_id: self.id).where(file_type: 'ClickWav').first + end + + def click_track + JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Click').first + end + + def has_count_in? + has_count_in = false + if jmep_json + jmep = JSON.parse(jmep_json) + + if jmep["Events"] + events = jmep["Events"] + metronome = nil + events.each do |event| + if event.has_key?("metronome") + metronome = event["metronome"] + break + end + end + if metronome + has_count_in = true + end + end + end + + has_count_in + end def master_track 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') + JamTrackTrack.where(jam_track_id: self.id).where("track_type = 'Track' or track_type = 'Click'") end def can_download?(user) @@ -431,6 +508,10 @@ module JamRuby jam_track_rights.where("user_id=?", user).first end + def mixdowns_for_user(user) + JamTrackMixdown.where(user_id: user.id).where(jam_track_id: self.id) + end + def short_plan_code prefix = 'jamtrack-' plan_code[prefix.length..-1] @@ -443,6 +524,86 @@ module JamRuby def generate_slug self.slug = sluggarize(original_artist) + '-' + sluggarize(name) + + if licensor && licensor.slug.present? + #raise "no slug on licensor #{licensor.id}" if licensor.slug.nil? + self.slug << "-" + licensor.slug + end + end + + def gen_plan_code + # remove all non-alphanumeric chars from artist as well as name + artist_code = original_artist.gsub(/[^0-9a-z]/i, '').downcase + name_code = name.gsub(/[^0-9a-z]/i, '').downcase + self.plan_code = "jamtrack-#{artist_code[0...20]}-#{name_code}" + + if licensor && licensor.slug + raise "no slug on licensor #{licensor.id}" if licensor.slug.nil? + self.plan_code << "-" + licensor.slug + end + + self.plan_code = self.plan_code[0...50] # make sure it's a max of 50 long + + + end + + def to_s + "#{self.name} (#{self.original_artist})" + end + + def self.latestPurchase(user_id) + JamTrackRight + .select('created_at') + .where(user_id: user_id) + .order('created_at DESC') + .limit(1) + .first + .try(:created_at) + .to_i + end + + attr_accessor :preview_generate_error + + before_save :jmep_json_generate + validate :jmep_text_validate + + def jmep_text_validate + begin + JmepManager.execute(self.jmep_text) + rescue ArgumentError => err + errors.add(:jmep_text, err.to_s) + end + end + + def jmep_json_generate + self.licensor_id = nil if self.licensor_id == '' + self.jmep_json = nil if self.jmep_json == '' + self.time_signature = nil if self.time_signature == '' + + begin + self[:jmep_json] = JmepManager.execute(self.jmep_text) + rescue ArgumentError => err + #errors.add(:jmep_text, err.to_s) + end + end + + # used in mobile simulate purchase + def self.forsale(user) + sql =< { order('created_at desc') }, class_name: "JamRuby::JamTrackMixdownPackage"#, order: 'created_at DESC' + has_one :jam_track_right, class_name: 'JamRuby::JamTrackRight', foreign_key: 'last_mixdown_id', inverse_of: :last_mixdown + + validates :name, presence: true, length: {maximum: 100} + validates :description, length: {maximum: 1000} + validates :user, presence: true + validates :jam_track, presence: true + validates :settings, presence: true + + validates_uniqueness_of :name, scope: [:user_id, :jam_track_id] + + validate :verify_settings + validate :verify_max_mixdowns + + def self.index(params, user) + jam_track_id = params[:id] + + limit = 20 + + query = JamTrackMixdown.where('jam_track_id = ?', jam_track_id).where('user_id = ?', user.id).order('created_at').paginate(page: 1, per_page: limit) + + count = query.total_entries + + if count == 0 + [query, nil, count] + elsif query.length < limit + [query, nil, count] + else + [query, start + limit, count] + end + end + + def verify_max_mixdowns + if self.jam_track && self.user && self.jam_track.mixdowns_for_user(self.user).length >= 5 + errors.add(:jam_track, 'allowed 5 mixes') + end + end + + def verify_settings + + # the user has to specify at least at least one tweak to volume, speed, pitch, pan. otherwise there is nothing to do + + #parsed = JSON.parse(self.settings) + parsed = self.settings + specified_track_count = parsed["tracks"] ? parsed["tracks"].length : 0 + + tweaked = false + all_quiet = jam_track.stem_tracks.length == 0 ? false : jam_track.stem_tracks.length == specified_track_count # we already say 'all_quiet is false' if the user did not specify as many tracks as there are on the JamTrack, because omission implies 'include this track' + + + if parsed["speed"] + tweaked = true + end + if parsed["pitch"] + tweaked = true + end + + + if parsed["tracks"] + parsed["tracks"].each do |track| + if track["mute"] + tweaked = true + end + if track["vol"] && track["vol"] != 0 + tweaked = true + end + if track["pan"] && track["pan"] != 0 + tweaked = true + end + + # there is at least one track with volume specified. + if !track["mute"] && track["vol"] != 0 + all_quiet = false + end + end + end + + if parsed["count-in"] + all_quiet = false + tweaked = true + end + + if all_quiet + errors.add(:settings, 'are all muted') + end + if !tweaked && !parsed['full'] + errors.add(:settings, 'have nothing specified') + end + + if parsed["speed"] && !parsed["speed"].is_a?(Integer) + errors.add(:settings, 'has non-integer speed') + end + + if parsed["pitch"] && !parsed["pitch"].is_a?(Integer) + errors.add(:settings, 'has non-integer pitch') + end + end + + def self.create(name, description, user, jam_track, settings) + mixdown = JamTrackMixdown.new + mixdown.name = name + mixdown.description = description + mixdown.user = user + mixdown.jam_track = jam_track + mixdown.settings = settings.to_json # RAILS 4 CAN REMOVE .to_json + mixdown.save + mixdown + end + + def will_pitch_shift? + self.settings["pitch"] != 0 || self.settings["speed"] != 0 + end + + def self.mixdownChecksum(user_id, jam_track_id) + dates = self + .select('created_at') + .where(user_id: user_id, jam_track_id: jam_track_id) + .order(:id) + + dates = dates.map do |date| + date.created_at.to_i.to_s + end.join('') + + Digest::MD5.hexdigest(dates) + end + + end +end + diff --git a/ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb b/ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb new file mode 100644 index 000000000..a6dfca754 --- /dev/null +++ b/ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb @@ -0,0 +1,255 @@ +module JamRuby + + # describes what users have rights to which tracks + class JamTrackMixdownPackage < ActiveRecord::Base + include JamRuby::S3ManagerMixin + + @@log = Logging.logger[JamTrackMixdownPackage] + + # these are used as extensions for the files stored in s3 + FILE_TYPE_MP3 = 'mp3' + FILE_TYPE_OGG = 'ogg' + FILE_TYPE_AAC = 'aac' + FILE_TYPES = [FILE_TYPE_MP3, FILE_TYPE_OGG, FILE_TYPE_AAC] + + SAMPLE_RATE_44 = 44 + SAMPLE_RATE_48 = 48 + SAMPLE_RATES = [SAMPLE_RATE_44, SAMPLE_RATE_48] + + ENCRYPT_TYPE_JKZ = 'jkz' + ENCRYPT_TYPES = [ENCRYPT_TYPE_JKZ, nil] + + default_scope { order('created_at desc') } + + belongs_to :jam_track_mixdown, class_name: "JamRuby::JamTrackMixdown", dependent: :destroy + + validates :jam_track_mixdown, presence: true + + validates :file_type, inclusion: {in: FILE_TYPES} + validates :sample_rate, inclusion: {in: SAMPLE_RATES} + validates :encrypt_type, inclusion: {in: ENCRYPT_TYPES} + validates_uniqueness_of :file_type, scope: [:sample_rate, :encrypt_type, :jam_track_mixdown_id] + validates :signing, inclusion: {in: [true, false]} + validates :signed, inclusion: {in: [true, false]} + + validate :verify_download_count + before_destroy :delete_s3_files + after_save :after_save + + MAX_JAM_TRACK_DOWNLOADS = 1000 + + def self.estimated_queue_time + jam_track_signing_count = JamTrackRight.where(queued: true).count + + #mixdowns = JamTrackMixdownPackage.unscoped.select('count(CASE WHEN queued THEN 1 ELSE NULL END) as queue_count, count(CASE WHEN speed_pitched THEN 1 ELSE NULL END) as speed_pitch_count').where(queued: true).limit(1) + mixdowns = ActiveRecord::Base.connection.execute("select count(CASE WHEN queued THEN 1 ELSE NULL END) as queue_count, count(CASE WHEN speed_pitched THEN 1 ELSE NULL END) as speed_pitch_count FROM jam_track_mixdown_packages WHERE queued = true")[0] + total_mixdowns = mixdowns['queue_count'].to_i + slow_mixdowns = mixdowns['speed_pitch_count'].to_i + fast_mixdowns = total_mixdowns - slow_mixdowns + + guess = APP_CONFIG.estimated_jam_track_time * jam_track_signing_count + APP_CONFIG.estimated_fast_mixdown_time * fast_mixdowns + APP_CONFIG.estimated_slow_mixdown_time * slow_mixdowns + + Stats.write('web.jam_track.queue_time', {value: guess / 60.0, jam_tracks: jam_track_signing_count, slow_mixdowns: slow_mixdowns, fast_mixdowns: fast_mixdowns}) + guess + end + + def after_save + # 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 || current_packaging_step != current_packaging_step_was || packaging_steps != packaging_steps_was + SubscriptionMessage.mixdown_signing_job_change(self) + end + end + + def self.create(mixdown, file_type, sample_rate, encrypt_type) + + package = JamTrackMixdownPackage.new + package.speed_pitched = mixdown.will_pitch_shift? + package.jam_track_mixdown = mixdown + package.file_type = file_type + package.sample_rate = sample_rate + package.signed = false + package.signing = false + package.encrypt_type = encrypt_type + package.save + package + 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}") + end + end + + def is_pitch_speed_shifted? + mix_settings = JSON.parse(self.settings) + mix_settings["speed"] || mix_settings["pitch"] + end + + def finish_errored(error_reason, error_detail) + self.last_errored_at = Time.now + 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 + self.signing = false + self.signing_queued_at = nil # if left set, throws off signing_state on subsequent signing attempts + + if save + Notification.send_mixdown_sign_failed(self) + else + raise "Error sending notification #{self.errors}" + end + end + + def finish_sign(url, private_key, length, md5) + self.url = url + self.private_key = private_key + self.signing_queued_at = nil # if left set, throws off signing_state on subsequent signing attempts + self.downloaded_since_sign = false + self.last_signed_at = Time.now + self.length = length + self.md5 = md5 + self.signed = true + self.signing = false + self.error_count = 0 + self.error_reason = nil + self.error_detail = nil + self.should_retry = false + save! + end + + def store_dir + "jam_track_mixdowns/#{created_at.strftime('%m-%d-%Y')}/#{self.jam_track_mixdown.user_id}" + end + + def filename + if encrypt_type + "#{id}.#{encrypt_type}" + else + "#{id}.#{file_type}" + end + end + + + # creates a short-lived URL that has access to the object. + # the idea is that this is used when a user who has the rights to this tries to download this JamTrack + # we would verify their rights (can_download?), and generates a URL in response to the click so that they can download + # but the url is short lived enough so that it wouldn't be easily shared + def sign_url(expiration_time = 120, content_type = nil, response_content_disposition = nil) + options = {:expires => expiration_time, :secure => true} + options[:response_content_type] = content_type if content_type + options[:response_content_disposition] = response_content_disposition if response_content_disposition + s3_manager.sign_url(self['url'], options) + end + + + def enqueue + begin + self.signing_queued_at = Time.now + self.signing_started_at = nil + self.last_signed_at = nil + self.queued = true + self.save + + queue_time = JamTrackMixdownPackage.estimated_queue_time + + # is_pitch_speed_shifted? + Resque.enqueue(JamTrackMixdownPackager, self.id) + return queue_time + rescue Exception => e + puts "e: #{e}" + # implies redis is down. we don't update started_at by bailing out here + false + end + end + + # 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 + state = signing_state + if state == 'SIGNED' || state == 'SIGNING' || state == 'QUEUED' + false + else + return enqueue + end + end + + def ready? + self.signed && self.url.present? + end + + # returns easy to digest state field + # SIGNED - the package is ready to be downloaded + # ERROR - the package was built unsuccessfully + # SIGNING_TIMEOUT - the package was kicked off to be signed, but it seems to have hung + # SIGNING - the package is currently signing + # 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 + state = nil + + if signed + state = 'SIGNED' + elsif signing_started_at && signing + # 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. + if Time.now - signing_started_at > APP_CONFIG.signing_job_signing_max_time + state = 'SIGNING_TIMEOUT' + elsif Time.now - last_step_at > APP_CONFIG.mixdown_step_max_time + state = 'SIGNING_TIMEOUT' + else + state = 'SIGNING' + end + elsif signing_queued_at + if Time.now - signing_queued_at > APP_CONFIG.mixdown_job_queue_max_time + state = 'QUEUED_TIMEOUT' + else + state = 'QUEUED' + end + elsif error_count > 0 + state = 'ERROR' + else + if Time.now - created_at > 60 # it should not take more than a minute to get QUIET out + state = 'QUIET_TIMEOUT' + else + state = 'QUIET' # needs to be poked to go build + end + + end + state + end + + def signed? + signed + end + + def update_download_count(count=1) + self.download_count = self.download_count + count + self.last_downloaded_at = Time.now + + if self.signed + self.downloaded_since_sign = true + end + end + + + def self.stats + stats = {} + + result = JamTrackMixdownPackage.unscoped.select('count(id) as total, count(CASE WHEN signing THEN 1 ELSE NULL END) as signing_count') + + stats['count'] = result[0]['total'].to_i + stats['signing_count'] = result[0]['signing_count'].to_i + stats + end + + + def delete_s3_files + s3_manager.delete(self.url) if self.url && s3_manager.exists?(self.url) + 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 135d8238c..aa389686a 100644 --- a/ruby/lib/jam_ruby/models/jam_track_right.rb +++ b/ruby/lib/jam_ruby/models/jam_track_right.rb @@ -11,7 +11,10 @@ module JamRuby 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 :jam_track, class_name: "JamRuby::JamTrack" + belongs_to :last_mixdown, class_name: 'JamRuby::JamTrackMixdown', foreign_key: 'last_mixdown_id', inverse_of: :jam_track_right + belongs_to :last_stem, class_name: 'JamRuby::JamTrackTrack', foreign_key: 'last_stem_id', inverse_of: :jam_track_right + validates :version, presence: true validates :user, presence: true validates :jam_track, presence: true validates :is_test_purchase, inclusion: {in: [true, false]} @@ -25,9 +28,16 @@ module JamRuby mount_uploader :url_48, JamTrackRightUploader mount_uploader :url_44, JamTrackRightUploader before_destroy :delete_s3_files + before_create :create_private_keys MAX_JAM_TRACK_DOWNLOADS = 1000 + def create_private_keys + rsa_key = OpenSSL::PKey::RSA.new(1024) + key = rsa_key.to_pem() + self.private_key_44 = key + self.private_key_48 = key + end def after_save # try to catch major transitions: @@ -58,6 +68,7 @@ module JamRuby def finish_errored(error_reason, error_detail, sample_rate) self.last_signed_at = Time.now + self.queued = false self.error_count = self.error_count + 1 self.error_reason = error_reason self.error_detail = error_detail @@ -77,6 +88,7 @@ module JamRuby def finish_sign(length, md5, bitrate) self.last_signed_at = Time.now + self.queued = false if bitrate==48 self.length_48 = length self.md5_48 = md5 @@ -99,10 +111,10 @@ module JamRuby # the idea is that this is used when a user who has the rights to this tries to download this JamTrack # we would verify their rights (can_download?), and generates a URL in response to the click so that they can download # but the url is short lived enough so that it wouldn't be easily shared - def sign_url(expiration_time = 120, bitrate=48) - field_name = (bitrate==48) ? "url_48" : "url_44" - s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => true}) - end + def sign_url(expiration_time = 120, bitrate=48, secure=true) + field_name = (bitrate==48) ? "url_48" : "url_44" + s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => secure}) + end def delete_s3_files remove_url_48! @@ -112,7 +124,7 @@ module JamRuby def enqueue(sample_rate=48) begin - 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) + 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, :queued => true) Resque.enqueue(JamTracksBuilder, self.id, sample_rate) true rescue Exception => e @@ -122,8 +134,33 @@ module JamRuby end end + def cleanup_old_package! + if self.jam_track.version != self.version + delete_s3_files + self[:url_48] = nil + self[:url_44] = nil + self.signing_queued_at = nil + self.signing_started_at_48 = nil + self.signing_started_at_44 = nil + self.last_signed_at = nil + self.current_packaging_step = nil + self.packaging_steps = nil + self.should_retry = false + self.signing_44 = false + self.signing_48 = false + self.signed_44 = false + self.signed_48 = false + self.queued = false + self.version = self.jam_track.version + self.save! + end + end # 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) + + # delete any package that's out dated + cleanup_old_package! + state = signing_state(sample_rate) if state == 'SIGNED' || state == 'SIGNING' || state == 'QUEUED' false @@ -137,9 +174,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_48 && self.url_48.present? && self.url_48.file.exists? + self.signed_48 && self.url_48.present? && self.url_48.file.exists? && self.version == self.jam_track.version else - self.signed_44 && self.url_44.present? && self.url_44.file.exists? + self.signed_44 && self.url_44.present? && self.url_44.file.exists? && self.version == self.jam_track.version end end diff --git a/ruby/lib/jam_ruby/models/jam_track_search.rb b/ruby/lib/jam_ruby/models/jam_track_search.rb new file mode 100644 index 000000000..a83e7e647 --- /dev/null +++ b/ruby/lib/jam_ruby/models/jam_track_search.rb @@ -0,0 +1,168 @@ +module JamRuby + class JamTrackSearch < BaseSearch + + cattr_accessor :jschema, :search_meta + attr_accessor :user_counters + + KEY_QUERY = 'query' + KEY_SEARCH_STR = 'search_str' + KEY_RESULT_TYPES = 'result_types' + KEY_SONGS = 'songs' + KEY_ARTISTS = 'artists' + KEY_RESULTS = 'results' + KEY_RESULT_SETS = 'result_sets' + KEY_PAGE_NUM = 'page_num' + KEY_TOTAL_COUNT = 'total_count' + KEY_PAGE_COUNT = 'page_count' + KEY_PER_PAGE = 'per_page' + PER_PAGE = 'development'==Rails.env ? 8 : 20 + KEY_GENRES = 'genres' + KEY_INSTRUMENTS = 'instruments' + KEY_LANGUAGE = 'language' + KEY_ORIGINAL_ARTIST = 'original_artist' + + def self.json_schema + return @@jschema ||= { + KEY_QUERY => { + KEY_SEARCH_STR => '', + KEY_INSTRUMENTS => [], + KEY_GENRES => [], + KEY_LANGUAGE => '', + KEY_ORIGINAL_ARTIST => '', + KEY_RESULT_TYPES => [], + KEY_PAGE_NUM => 1, + KEY_PER_PAGE => PER_PAGE, + }, + KEY_RESULT_SETS => { + KEY_SONGS => { + KEY_RESULTS => [], + KEY_PAGE_NUM => 1, + KEY_TOTAL_COUNT => 0, + KEY_PAGE_COUNT => 0, + }, + KEY_ARTISTS => { + KEY_RESULTS => [], + KEY_PAGE_NUM => 1, + KEY_TOTAL_COUNT => 0, + KEY_PAGE_COUNT => 0, + }, + }, + } + end + + def self.search_target_class + JamTrack + end + + def do_search(query) + rel = JamTrack.unscoped + + unless (gids = query[KEY_GENRES]).blank? + allgids = self.class.genre_ids + gids = gids.select { |gg| allgids.has_key?(gg) } + + unless gids.blank? + sqlstr = "'#{gids.join("','")}'" + rel = rel.joins(:genres_jam_tracks) + rel = rel.where("genres_jam_tracks.genre_id IN (#{sqlstr})") + end + end + unless (instruments = query[KEY_INSTRUMENTS]).blank? + instrids = self.class.instrument_ids + instruments = instruments.select { |ii| instrids.has_key?(ii['instrument_id']) } + + unless instruments.blank? + sqlstr = "'#{instruments.join("','")}'" + rel = rel.joins(:jam_track_tracks) + rel = rel.where("jam_track_tracks.instrument_id IN (#{sqlstr})") + rel = rel.where("jam_track_tracks.track_type = 'Track'") + end + end + + unless (artist_name = query[KEY_ORIGINAL_ARTIST]).blank? + rel = rel.where(original_artist: artist_name) + end + + rel + end + + def search_results_page(query=nil) + filter = { + KEY_QUERY => query, + } + result_types = query[KEY_RESULT_TYPES] + if result_types + has_songs, has_artists = result_types.index(KEY_SONGS), result_types.index(KEY_ARTISTS) + else + has_songs, has_artists = true, true + end + result_sets = filter[KEY_RESULT_SETS] = self.class.json_schema[KEY_RESULT_SETS].clone + if has_songs + rel = do_search(query) + unless (val = query[KEY_SEARCH_STR]).blank? + tsquery = Search.create_tsquery(val) + rel = rel.where("(search_tsv @@ to_tsquery('jamenglish', ?))", tsquery) if tsquery + end + rel = rel.order(:name).includes(:genres) + + pgnum = [query[KEY_PAGE_NUM].to_i, 1].max + rel = rel.paginate(:page => pgnum, :per_page => query[KEY_PER_PAGE]) + + results = rel.all.collect do |jt| + { + 'id' => jt.id, + 'name' => jt.name, + 'artist' => jt.original_artist, + 'genre' => jt.genres.map(&:description).join(', '), + 'plan_code' => jt.plan_code, + 'year' => jt.year + } + end + + result_sets[KEY_SONGS] = { + KEY_RESULTS => results, + KEY_PAGE_NUM => pgnum, + KEY_TOTAL_COUNT => rel.total_entries, + KEY_PAGE_COUNT => rel.total_pages, + } + end + + if has_artists + rel = do_search(query) + counter = rel.select("DISTINCT(jam_tracks.original_artist)") + rel = rel.select("DISTINCT ON(jam_tracks.original_artist) jam_tracks.id, jam_tracks.original_artist") + + unless (val = query[KEY_SEARCH_STR]).blank? + rel = rel.where("original_artist ILIKE ?","%#{val}%") + counter = counter.where("original_artist ILIKE ?","%#{val}%") + end + rel = rel.order(:original_artist) + + pgnum = [query[KEY_PAGE_NUM].to_i, 1].max + rel = rel.paginate(:page => pgnum, :per_page => query[KEY_PER_PAGE]) + + results = rel.all.collect do |jt| + { 'id' => jt.id, 'artist' => jt.original_artist } + end + + artist_count = counter.count + + result_sets[KEY_ARTISTS] = { + KEY_RESULTS => results, + KEY_PAGE_NUM => pgnum, + KEY_TOTAL_COUNT => artist_count, + KEY_PAGE_COUNT => (artist_count / query[KEY_PER_PAGE].to_f).ceil, + } + end + + filter + end + + def self.all_languages + JamTrack.select("SELECT DISTINCT(language)").order(:language).collect do |lang| + { description: ISO_639.find_by_code(lang), id: lang } + end + end + + end +end diff --git a/ruby/lib/jam_ruby/models/jam_track_session.rb b/ruby/lib/jam_ruby/models/jam_track_session.rb new file mode 100644 index 000000000..6e244e83b --- /dev/null +++ b/ruby/lib/jam_ruby/models/jam_track_session.rb @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +module JamRuby + class JamTrackSession < ActiveRecord::Base + + BROWSER = 'browser' + SESSION = 'session' + TYPES = [BROWSER, SESSION] + + @@log = Logging.logger[JamTrackSession] + + belongs_to :music_session, class_name: 'JamRuby::MusicSession' + belongs_to :user, class_name: 'JamRuby::User' + belongs_to :jam_track, class_name: 'JamRuby::JamTrack' + + def self.create_session(jam_track, user, music_session) + create(jam_track, user, SESSION, music_session) + end + + def self.create_browser(jam_track, user) + create(jam_track, user, BROWSER, nil) + end + + private + def self.create(jam_track, user, type, music_session) + jam_track_session = JamTrackSession.new + jam_track_session.jam_track = jam_track + jam_track_session.user = user + jam_track_session.session_type = type + jam_track_session.music_session = music_session + jam_track_session.save + jam_track_session + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/jam_track_track.rb b/ruby/lib/jam_ruby/models/jam_track_track.rb index c0548e6fd..77938d0c3 100644 --- a/ruby/lib/jam_ruby/models/jam_track_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track_track.rb @@ -6,7 +6,7 @@ module JamRuby include JamRuby::S3PublicManagerMixin # there should only be one Master per JamTrack, but there can be N Track per JamTrack - TRACK_TYPE = %w{Track Master} + TRACK_TYPE = %w{Track Master Click} @@log = Logging.logger[JamTrackTrack] @@ -19,7 +19,7 @@ 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, :preview_generate_error + attr_accessor :original_audio_s3_path, :skip_uploader, :preview_generate_error, :wav_file, :tmp_duration, :skip_inst_part_uniq before_destroy :delete_s3_files @@ -27,29 +27,44 @@ module JamRuby validates :part, length: {maximum: 35} validates :track_type, inclusion: {in: TRACK_TYPE } validates :preview_start_time, numericality: {only_integer: true}, length: {in: 1..1000}, :allow_nil => true - validates_uniqueness_of :part, scope: [:jam_track_id, :instrument_id] + validates_uniqueness_of :part, scope: [:jam_track_id, :instrument_id], unless: :skip_inst_part_uniq # validates :jam_track, presence: true belongs_to :instrument, class_name: "JamRuby::Instrument" belongs_to :jam_track, class_name: "JamRuby::JamTrack" has_many :recorded_jam_track_tracks, :class_name => "JamRuby::RecordedJamTrackTrack", :foreign_key => :jam_track_track_id, :dependent => :destroy + has_one :jam_track_right, class_name: 'JamRuby::JamTrackRight', foreign_key: 'last_stem_id', inverse_of: :last_stem # create storage directory that will house this jam_track, as well as def store_dir "jam_track_tracks" end + + def licensor_suffix + suffix = '' + if jam_track.licensor + raise "no licensor name" if jam_track.licensor.name.nil? + suffix = " - #{jam_track.licensor.name}" + end + suffix + end + # create name of the file def filename(original_name) - "#{store_dir}/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}" + "#{store_dir}/#{jam_track.original_artist}/#{jam_track.name}#{licensor_suffix}/#{original_name}" end # create name of the preview file. # md5-'ed because we cache forever def preview_filename(md5, ext='ogg') original_name = "#{File.basename(self["url_44"], ".ogg")}-preview-#{md5}.#{ext}" - "jam_track_previews/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}" + "#{preview_directory}/#{original_name}" + end + + def preview_directory + "jam_track_previews/#{jam_track.original_artist}/#{jam_track.name}#{licensor_suffix}" end def has_preview? @@ -58,7 +73,16 @@ module JamRuby # generates a URL that points to a public version of the preview def preview_public_url(media_type='ogg') - url = media_type == 'ogg' ? self[:preview_url] : self[:preview_mp3_url] + case media_type + when 'ogg' + url = self[:preview_url] + when 'mp3' + url = self[:preview_mp3_url] + when 'aac' + url = self[:preview_aac_url] + else + raise "unknown media_type #{media_type}" + end if url s3_public_manager.public_url(url,{ :secure => true}) else @@ -66,6 +90,18 @@ module JamRuby end end + def display_name + if track_type == 'Master' + 'Master Mix' + else + display_part = '' + if part + display_part = "-(#{part})" + end + "#{instrument.description}#{display_part}" + end + end + def manually_uploaded_filename(mounted_as) if track_type == 'Master' filename("Master Mix-#{mounted_as == :url_48 ? '48000' : '44100'}.ogg") @@ -89,6 +125,18 @@ module JamRuby 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 => true}) end + + def web_download_sign_url(expiration_time = 120, type='mp3', content_type = nil, response_content_disposition = nil) + options = {:expires => expiration_time, :secure => true} + options[:response_content_type] = content_type if content_type + options[:response_content_disposition] = response_content_disposition if response_content_disposition + + url_field = self['url_' + type + '_48'] + url_field = self['url_48'] if type == 'ogg' # ogg has different column format in database + + + s3_manager.sign_url(url_field, options) + end def can_download?(user) # I think we have to make a special case for 'previews', but maybe that's just up to the controller to not check can_download? @@ -157,6 +205,7 @@ module JamRuby uuid = SecureRandom.uuid output = File.join(tmp_dir, "#{uuid}.ogg") output_mp3 = File.join(tmp_dir, "#{uuid}.mp3") + output_aac = File.join(tmp_dir, "#{uuid}.aac") start = self.preview_start_time.to_f / 1000 stop = start + 20 @@ -176,7 +225,6 @@ module JamRuby # 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}` @@ -187,35 +235,55 @@ module JamRuby @@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) + convert_aac_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{output}\" -c:a libfdk_aac -b:a 192k \"#{output_aac}\"" + @@log.debug("converting to aac using: " + convert_aac_cmd) - self.skip_uploader = true + convert_output = `#{convert_aac_cmd}` - original_ogg_preview_url = self["preview_url"] - original_mp3_preview_url = self["preview_mp3_url"] + result_code = $?.to_i - # 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 result_code != 0 + @@log.debug("fail #{result_code}") + @preview_generate_error = "unable to execute aac convert command #{convert_output}" + else - # 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" + ogg_digest = ::Digest::MD5.file(output) + mp3_digest = ::Digest::MD5.file(output_mp3) + aac_digest = ::Digest::MD5.file(output_aac) + self["preview_md5"] = ogg_md5 = ogg_digest.hexdigest + self["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest + self["preview_aac_md5"] = aac_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) + @@log.debug("uploading aac preview to #{self.preview_filename('aac')}") + s3_public_manager.upload(self.preview_filename(aac_md5, 'aac'), output_aac, content_type: 'audio/aac', content_md5: aac_digest.base64digest) + + self.skip_uploader = true + + original_ogg_preview_url = self["preview_url"] + original_mp3_preview_url = self["preview_mp3_url"] + original_aac_preview_url = self["preview_aac_url"] + + self["preview_url"] = self.preview_filename(ogg_md5, 'ogg') + self["preview_length"] = File.new(output).size + self["preview_mp3_url"] = self.preview_filename(mp3_md5, 'mp3') + self["preview_mp3_length"] = File.new(output_mp3).size + self["preview_aac_url"] = self.preview_filename(aac_md5, 'aac') + self["preview_aac_length"] = File.new(output_aac).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"] + s3_public_manager.delete(original_aac_preview_url) if original_aac_preview_url && original_aac_preview_url != track["preview_aac_url"] + rescue + puts "UNABLE TO CLEANUP OLD PREVIEW URL" + end end end diff --git a/ruby/lib/jam_ruby/models/jamblaster.rb b/ruby/lib/jam_ruby/models/jamblaster.rb new file mode 100644 index 000000000..b1e19bfc1 --- /dev/null +++ b/ruby/lib/jam_ruby/models/jamblaster.rb @@ -0,0 +1,42 @@ +module JamRuby + class Jamblaster < ActiveRecord::Base + + attr_accessible :user_id, :serial_no, :client_id, :user_ids, as: :admin + + + belongs_to :user, class_name: 'JamRuby::User' + has_many :jamblasters_users, class_name: "JamRuby::JamblasterUser" + has_many :users, class_name: 'JamRuby::User', through: :jamblasters_users + has_many :jamblaster_pairing_requests, class_name: "JamRuby::JamblasterPairingRequest", foreign_key: :jamblaster_id + belongs_to :connection, class_name: "JamRuby::Connection", foreign_key: "client_id" + validates :user, presence: true + + validates :serial_no, uniqueness: true + validates :client_id, uniqueness: true + + before_save :sanitize_active_admin + + def sanitize_active_admin + self.client_id = nil if self.client_id == '' + self.serial_no = nil if self.serial_no == '' + end + + def most_recent_pairing + jamblaster_pairing_requests.where(active: true).first + end + + class << self + + @@mq_router = MQRouter.new + @@message_factory = MessageFactory.new + + def send_pair_attempt(jbid, scid, vtoken) + msg = @@message_factory.pair_attempt( + jbid, scid, vtoken + ) + + @@mq_router.publish_to_client(jbid, msg, {:client_id => scid}) + end + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/jamblaster_pairing_request.rb b/ruby/lib/jam_ruby/models/jamblaster_pairing_request.rb new file mode 100644 index 000000000..a198353c0 --- /dev/null +++ b/ruby/lib/jam_ruby/models/jamblaster_pairing_request.rb @@ -0,0 +1,28 @@ +module JamRuby + class JamblasterPairingRequest < ActiveRecord::Base + + belongs_to :user, class_name: 'JamRuby::User' + belongs_to :jamblaster, class_name: 'JamRuby::Jamblaster', foreign_key: :jamblaster_id + + validates :user, presence: true + validates :jamblaster, presence: true + validates :jamblaster_client_id, presence: true + validates :vtoken, presence: true + + def key + sibling_key + end + + def activate(key) + JamblasterPairingRequest.transaction do + JamblasterPairingRequest.where(jamblaster_id: jamblaster_id).update_all(active: false) + self.active = true + self.sibling_key = key + if !self.save + raise ActiveRecord::Rollback + end + end + end + + end +end diff --git a/ruby/lib/jam_ruby/models/jamblaster_user.rb b/ruby/lib/jam_ruby/models/jamblaster_user.rb new file mode 100644 index 000000000..97718c9df --- /dev/null +++ b/ruby/lib/jam_ruby/models/jamblaster_user.rb @@ -0,0 +1,11 @@ +module JamRuby + class JamblasterUser < ActiveRecord::Base + self.table_name = "jamblasters_users" + + belongs_to :jamblaster, class_name: "JamRuby::Jamblaster" + belongs_to :user, class_name: "JamRuby::User" + + validates :jamblaster, presence:true + validates :user, presence: true + end +end diff --git a/ruby/lib/jam_ruby/models/language.rb b/ruby/lib/jam_ruby/models/language.rb new file mode 100644 index 000000000..4492f800b --- /dev/null +++ b/ruby/lib/jam_ruby/models/language.rb @@ -0,0 +1,13 @@ +module JamRuby + class Language < ActiveRecord::Base + include HtmlSanitize + html_sanitize strict: [:name, :description] + has_many :teachers, :class_name => "JamRuby::Teacher", :through => :teachers_languages + has_many :teachers_languages, class_name: "JamRuby::TeacherLanguage" + + def self.english_sort + languages = Language.order(:description) + languages.sort_by { |l| [ l.id == 'EN' ? 0 : 1, l.description] } + end + end +end diff --git a/ruby/lib/jam_ruby/models/lesson_booking.rb b/ruby/lib/jam_ruby/models/lesson_booking.rb new file mode 100644 index 000000000..505257dc3 --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_booking.rb @@ -0,0 +1,1016 @@ +# represenst the type of lesson package +module JamRuby + class LessonBooking < ActiveRecord::Base + + include HtmlSanitize + html_sanitize strict: [:description, :cancel_message] + + include ActiveModel::Dirty + + @@log = Logging.logger[LessonBooking] + + attr_accessor :accepting, :countering, :canceling, :autocanceling, :countered_slot, :countered_lesson, :current_purchase, :current_lesson + + STATUS_REQUESTED = 'requested' + STATUS_CANCELED = 'canceled' + STATUS_APPROVED = 'approved' + STATUS_SUSPENDED = 'suspended' + STATUS_COUNTERED = 'countered' + STATUS_COMPLETED = 'completed' + STATUS_UNCONFIRMED = 'unconfirmed' + + STATUS_TYPES = [STATUS_REQUESTED, STATUS_CANCELED, STATUS_APPROVED, STATUS_SUSPENDED, STATUS_COUNTERED, STATUS_COMPLETED, STATUS_UNCONFIRMED] + + LESSON_TYPE_FREE = 'single-free' + LESSON_TYPE_TEST_DRIVE = 'test-drive' + LESSON_TYPE_PAID = 'paid' + + LESSON_TYPES = [LESSON_TYPE_FREE, LESSON_TYPE_TEST_DRIVE, LESSON_TYPE_PAID] + + PAYMENT_STYLE_ELSEWHERE = 'elsewhere' + PAYMENT_STYLE_SINGLE = 'single' + PAYMENT_STYLE_WEEKLY = 'weekly' + PAYMENT_STYLE_MONTHLY = 'monthly' + + ENGAGED = "status = '#{STATUS_APPROVED}' OR status = '#{STATUS_REQUESTED}' OR status = '#{STATUS_SUSPENDED}'" + + + PAYMENT_STYLES = [PAYMENT_STYLE_ELSEWHERE, PAYMENT_STYLE_SINGLE, PAYMENT_STYLE_WEEKLY, PAYMENT_STYLE_MONTHLY] + + belongs_to :user, class_name: "JamRuby::User" + belongs_to :teacher, class_name: "JamRuby::User" + belongs_to :accepter, class_name: "JamRuby::User" + belongs_to :canceler, class_name: "JamRuby::User" + belongs_to :counterer, class_name: "JamRuby::User", foreign_key: :counterer_id + belongs_to :default_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :default_slot_id, inverse_of: :defaulted_booking, :dependent => :destroy + belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_booking, :dependent => :destroy + belongs_to :school, class_name: "JamRuby::School" + belongs_to :test_drive_package_choice, class_name: "JamRuby::TestDrivePackageChoice" + has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot", :dependent => :destroy + has_many :lesson_sessions, class_name: "JamRuby::LessonSession", :dependent => :destroy + has_many :lesson_package_purchases, class_name: "JamRuby::LessonPackagePurchase", :dependent => :destroy + + validates :user, presence: true + validates :teacher, presence: true + validates :lesson_type, inclusion: {in: LESSON_TYPES} + validates :status, presence: true, inclusion: {in: STATUS_TYPES} + validates :recurring, inclusion: {in: [true, false]} + validates :sent_notices, inclusion: {in: [true, false]} + validates :card_presumed_ok, inclusion: {in: [true, false]} + validates :same_school, inclusion: {in: [true, false]} + validates :active, inclusion: {in: [true, false]} + validates :lesson_length, inclusion: {in: [30, 45, 60, 90, 120]} + validates :payment_style, inclusion: {in: PAYMENT_STYLES} + validates :booked_price, presence: true + validates :description, no_profanity: true, length: {minimum: 10, maximum: 20000} + + validate :validate_user, on: :create + validate :validate_recurring + validate :validate_lesson_booking_slots + validate :validate_lesson_length + validate :validate_payment_style + validate :validate_uncollectables, on: :create + validate :validate_accepted, :if => :accepting + validate :validate_canceled, :if => :canceling + + + before_save :before_save + before_validation :before_validation + after_create :after_create + around_save :around_update + + scope :test_drive, -> { where(lesson_type: LESSON_TYPE_TEST_DRIVE) } + scope :active, -> { where(active: true) } + scope :approved, -> { where(status: STATUS_APPROVED) } + scope :requested, -> { where(status: STATUS_REQUESTED) } + scope :canceled, -> { where(status: STATUS_CANCELED) } + scope :suspended, -> { where(status: STATUS_SUSPENDED) } + scope :engaged, -> { where(ENGAGED) } + scope :engaged_or_successful, -> { where("(" + ENGAGED + ") OR (lesson_bookings.status = '#{STATUS_COMPLETED}' AND lesson_bookings.success = true)")} + + def before_validation + if self.booked_price.nil? + self.booked_price = compute_price + end + end + + def after_create + if (card_presumed_ok || school_on_school?) && !sent_notices + send_notices + end + end + + def before_save + automatically_default_slot + end + + def around_update + + @default_slot_did_change = self.default_slot_id_changed? + + yield + + sync_lessons + sync_remaining_test_drives + + @default_slot_did_change = nil + @accepting = nil + @countering = nil + end + + # here for shopping_cart + def product_info + if is_test_drive? + real_price = 0 + elsif is_monthly_payment? + raise "no purchase assigned to lesson booking for monthly payment!" if current_purchase.nil? + real_price = self.current_purchase.teacher_distribution.jamkazam_margin + else + if current_lesson.nil? + puts "OHOHOMOOMG #{self.inspect}" + raise "no purchase assigned to lesson booking for lesson!" + end + + real_price = self.current_lesson.teacher_distribution.jamkazam_margin + end + {price: real_price, real_price: real_price, total_price: real_price} + end + # here for shopping_cart + def price + booked_price + end + + def no_slots + default_slot.from_package + end + + def alt_slot + found = nil + lesson_booking_slots.each do |slot| + if slot.id != default_slot.id + found = slot + break + end + end + found + end + + def student + user + end + + def next_lesson + if recurring + session = lesson_sessions.joins(:music_session).where("scheduled_start is not null").where("scheduled_start > ?", Time.now).order(:created_at).first + if session.nil? + session = lesson_sessions[0] + end + LessonSession.find(session.id) if session + else + lesson_sessions[0] + end + end + + def accept(lesson_session, slot, accepter) + if !is_active? + self.accepting = true + end + + self.active = true + self.status = STATUS_APPROVED + self.counter_slot = nil + self.default_slot = slot + self.accepter = accepter + + success = self.save + + if !success + puts "unable to accept lesson booking #{errors.inspect}" + end + success + end + + def counter(lesson_session, proposer, slot) + self.countering = true + self.lesson_booking_slots << slot + self.counter_slot = slot + self.counterer = proposer + self.countered_at = Time.now + self.sent_counter_reminder = false + + if self.default_slot.from_package + self.default_slot = slot + end + #self.status = STATUS_COUNTERED + self.save + end + + def automatically_default_slot + if is_requested? && default_slot.nil? + if lesson_booking_slots.length > 0 + self.default_slot = lesson_booking_slots[0] + end + end + end + + def sync_remaining_test_drives + if is_test_drive? || is_single_free? + if card_presumed_ok && !user_decremented + self.user_decremented = true + self.save(validate: false) + if is_single_free? + user.remaining_free_lessons = user.remaining_free_lessons - 1 + elsif is_test_drive? + user.remaining_test_drives = user.remaining_test_drives - 1 + end + user.save(validate: false) + end + end + end + + def create_minimum_booking_time + # trying to be too smart + #(Time.now + APP_CONFIG.minimum_lesson_booking_hrs * 60*60) + Time.now + end + + def sync_lessons + + if is_canceled? || is_completed? + # don't create new sessions if cancelled + return + end + + + if @default_slot_did_change + + end + # Here we go; let's create a lesson(s) as needed + + # we need to make lessons into the future a bit, to give time for everyone involved + minimum_start_time = create_minimum_booking_time + + # get all sessions that are already scheduled for this booking ahead of the minimum time + + sessions= MusicSession.joins(:lesson_session).where("lesson_sessions.lesson_booking_id = ?", id).where("scheduled_start is not null").order(:created_at) + if recurring + # only want times ahead of this for recurring + sessions = sessions.where("scheduled_start > ?", minimum_start_time) + end + + if @default_slot_did_change + # # adjust all session times + + offset = 0 + sessions.each_with_index do |item, i| + item.lesson_session.slot = default_slot + result = item.lesson_session.update_next_available_time(offset) + if result + offset = result + offset += 1 + end + end + end + + needed_sessions = determine_needed_sessions(sessions) + + # if the latest scheduled session is after the minimum start time, then bump up minimum start time + last_session = sessions.last + last_session.reload if last_session # because of @default_slot_did_change logic above, this can be necessary + + if last_session && last_session.scheduled_start && last_session.scheduled_start > minimum_start_time + minimum_start_time = last_session.scheduled_start + end + times = default_slot.scheduled_times(needed_sessions, minimum_start_time) + + scheduled_lessons(times) + end + + # sensitive to current time + def predicted_times_for_month(year, month) + first_day = Date.new(year, month, 1) + last_day = Date.new(year, month, -1) + sessions = MusicSession.joins(:lesson_session).where("lesson_sessions.lesson_booking_id = ?", id).where("scheduled_start >= ?", first_day).where("scheduled_start <= ?", last_day).order(:created_at) + + times = [] + + sessions.each do |session| + times << session.scheduled_start + end + last_session = sessions.last + + start_day = first_day + if last_session + start_day = last_session.scheduled_start.to_date + 1 + end + + # now flesh out the rest of the month with predicted times + + more_times = default_slot.scheduled_times(5, start_day) + + more_times.each do |time| + if time.to_date >= first_day && time.to_date <= last_day + times << time + end + end + { times: times, session: sessions.first } + end + + def determine_needed_sessions(sessions) + needed_sessions = 0 + if is_requested? + # in the case of a requested booking (not approved) only make one, even if it's recurring. This is for UI considerations + if sessions.count == 0 + needed_sessions = 1 + end + elsif is_active? + expected_num_sessions = recurring ? 2 : 1 + needed_sessions = expected_num_sessions - sessions.count + end + needed_sessions + end + + def scheduled_lessons(times) + times.each do |time| + + lesson_session = LessonSession.create(self) + + if lesson_session.errors.any? + puts "JamClass lesson session creation errors #{lesson_session.errors.inspect}" + @@log.error("JamClass lesson session creation errors #{lesson_session.errors.inspect}") + raise ActiveRecord::Rollback + end + ms_tz = ActiveSupport::TimeZone.new(default_slot.timezone) + ms_tz = "#{ms_tz.name},#{default_slot.timezone}" + + rsvps = [{instrument_id: 'other', proficiency_level: 0, approve: true}] + + music_session = MusicSession.create(student, { + name: "#{display_type2} JamClass taught by #{teacher.name}", + description: "This is a #{lesson_length}-minute #{display_type2} lesson with #{teacher.name}.", + musician_access: false, + fan_access: false, + genres: ['other'], + approval_required: false, + fan_chat: false, + legal_policy: "standard", + language: 'eng', + duration: lesson_length, + recurring_mode: false, + timezone: ms_tz, + create_type: MusicSession::CREATE_TYPE_LESSON, + is_unstructured_rsvp: true, + scheduled_start: time, + invitations: [teacher.id], + lesson_session: lesson_session, + rsvp_slots: rsvps + }) + + if music_session.errors.any? + puts "JamClass lesson scheduling errors #{music_session.errors.inspect}" + @@log.error("JamClass lesson scheduling errors #{music_session.errors.inspect}") + raise ActiveRecord::Rollback + end + + if lesson_session.is_active? + # send out email to student to act as something they can add to their calendar + Notification.send_student_jamclass_invitation(music_session, student) + end + + end + end + + def is_weekly_payment? + payment_style == PAYMENT_STYLE_WEEKLY + end + + def is_monthly_payment? + payment_style == PAYMENT_STYLE_MONTHLY + end + + def requires_per_session_billing? + is_normal? && !is_monthly_payment? + end + + def requires_teacher_distribution?(target) + if school_on_school? + false + elsif target.is_a?(JamRuby::LessonSession) + is_test_drive? || (is_normal? && !is_monthly_payment?) + elsif target.is_a?(JamRuby::LessonPackagePurchase) + is_monthly_payment? + else + raise "unable to determine object type of #{target}" + end + end + + def is_requested? + status == STATUS_REQUESTED + end + + def is_canceled? + status == STATUS_CANCELED + end + + def is_completed? + status == STATUS_COMPLETED + end + + def is_approved? + status == STATUS_APPROVED + end + + def is_suspended? + status == STATUS_SUSPENDED + end + + def is_active? + active + end + + def validate_accepted + # accept is multipe purpose; either accept the initial request, or a counter slot + if self.status_was != STATUS_REQUESTED && counter_slot.nil? # && self.status_was != STATUS_COUNTERED + self.errors.add(:status, "This lesson is already #{self.status}.") + end + + if self.accepter.nil? + self.errors.add(:accepter, "No one has been indicated as accepting the lesson") + end + self.accepting = false + end + + def validate_canceled + if !is_canceled? + self.errors.add(:status, "This session is already #{self.status}.") + end + + self.canceling = false + end + + def send_notices + UserMailer.student_lesson_request(self).deliver_now + UserMailer.teacher_lesson_request(self).deliver_now + Notification.send_lesson_message('requested', lesson_sessions[0], false) # TODO: this isn't quite an 'accept' + self.sent_notices = true + self.sent_notices_at = Time.now + self.save + end + + def resolved_test_drive_package + result = nil + purchase = student.most_recent_test_drive_purchase + if purchase + # for lessons already packaged + result = purchase.lesson_package_type + else + # for unbooked lessons + result = student.desired_package + end + if result.nil? + result = LessonPackageType.test_drive_4 + end + result + end + + def lesson_package_type + if is_single_free? + LessonPackageType.single_free + elsif is_test_drive? + resolved_test_drive_package + elsif is_normal? + LessonPackageType.single + end + end + + def display_type2 + if is_single_free? + "Free" + elsif is_test_drive? + "TestDrive" + elsif is_normal? + "Single" + end + end + + def display_type + if is_single_free? + "Free" + elsif is_test_drive? + "TestDrive" + elsif is_normal? + if recurring + "recurring" + else + "single" + end + + end + end + + # determine the price of this booking based on what the user wants, and the teacher's pricing + + def compute_price + if is_single_free? + 0 + elsif is_test_drive? + resolved_test_drive_package.price + elsif is_normal? + teacher.teacher.booking_price(lesson_length, payment_style != PAYMENT_STYLE_MONTHLY) + end + end + + def distribution_price_in_cents(target) + if is_single_free? + 0 + elsif is_test_drive? + 10 * 100 + elsif is_normal? + if is_monthly_payment? + raise "not a LessonPackagePurchase: #{target.inspect}" if !target.is_a?(LessonPackagePurchase) + + today = Date.today + + start_date = Date.new(target.year, target.month, 1) + if today.year == target.year && today.month == target.month + # we are in the month being billed. we should set the start date based on today + start_date = today + end + (LessonSessionMonthlyPrice.price(self, start_date) * 100).round + else + booked_price * 100 + end + end + end + + def is_single_free? + lesson_type == LESSON_TYPE_FREE + end + + def is_test_drive? + lesson_type == LESSON_TYPE_TEST_DRIVE + end + + def is_normal? + lesson_type == LESSON_TYPE_PAID + end + + def dayWeekDesc(slot = default_slot) + day = case slot.day_of_week + when 0 then "Sunday" + when 1 then "Monday" + when 2 then "Tuesday" + when 3 then "Wednesday" + when 4 then "Thursday" + when 5 then "Friday" + when 6 then "Saturday" + end + + + if slot.hour > 11 + hour = slot.hour - 12 + if hour == 0 + hour = 12 + end + am_pm = 'pm' + else + hour = slot.hour + if hour == 0 + hour = 12 + end + am_pm = 'am' + end + + "#{day} at #{hour}:#{slot.minute}#{am_pm}" + + end + + def approved_before? + !self.accepter_id.nil? + end + + def autocancel + self.autocanceling = true + self.active = false + self.status = STATUS_UNCONFIRMED + save + self + end + def cancel(canceler, other, message) + + self.canceling = true + self.active = false + self.status = STATUS_CANCELED + self.cancel_message = message + self.canceler = canceler + success = save + if success + lesson_sessions.past_cancel_window.each do |lesson_session| + lesson_session = LessonSession.find(lesson_session.id) # because .upcoming creates ReadOnly records + lesson_session.cancel_lesson(canceler, message) + if !lesson_session.save + return lesson_session + end + end + if approved_before? + # just tell both people it's cancelled, to act as confirmation + Notification.send_lesson_message('canceled', next_lesson, false) + Notification.send_lesson_message('canceled', next_lesson, true) + UserMailer.student_lesson_booking_canceled(self, message).deliver_now + UserMailer.teacher_lesson_booking_canceled(self, message).deliver_now + purpose = "Lesson Canceled" + else + if canceler == student + # if it's the first time acceptance student canceling, we call it a 'cancel' + Notification.send_lesson_message('canceled', next_lesson, false) + UserMailer.teacher_lesson_booking_canceled(self, message).deliver_now + purpose = "Lesson Canceled" + else + # if it's the first time acceptance teacher, it was declined + UserMailer.student_lesson_booking_declined(self, message).deliver_now + Notification.send_lesson_message('declined', next_lesson, true) + purpose = "Lesson Declined" + end + end + + message = '' if message.nil? + msg = ChatMessage.create(canceler, nil, message, ChatMessage::CHANNEL_LESSON, nil, other, next_lesson, purpose) + else + end + + + self + end + + def card_approved + self.card_presumed_ok = true + if self.save && !sent_notices + send_notices + end + end + + def validate_user + if card_presumed_ok && is_single_free? + if !user.has_free_lessons? + errors.add(:user, 'have no remaining free lessons') + end + + #if !user.has_stored_credit_card? + # errors.add(:user, 'has no credit card stored') + #end + elsif is_test_drive? + if !user.has_test_drives? && !user.can_buy_test_drive? + errors.add(:user, "have no remaining test drives") + elsif teacher.has_booked_test_drive_with_student?(user) && !user.admin + errors.add(:user, "have an in-progress or successful TestDrive with this teacher already") + end + + + elsif is_normal? + #if !user.has_stored_credit_card? + # errors.add(:user, 'has no credit card stored') + #end + end + end + + def validate_teacher + # shouldn't we check if the teacher already has a booking in this time slot, or at least warn the user + end + + def validate_recurring + if is_single_free? || is_test_drive? + if recurring + errors.add(:recurring, "can not be true for this type of lesson") + end + end + + false + end + + def validate_lesson_booking_slots + if test_drive_package_choice.nil? + if lesson_booking_slots.length == 0 || lesson_booking_slots.length == 1 + errors.add(:lesson_booking_slots, "must have two times specified") + end + end + end + + def validate_lesson_length + if is_single_free? || is_test_drive? + if lesson_length != 30 + errors.add(:lesson_length, "must be 30 minutes") + end + end + end + + def validate_payment_style + if is_normal? + if payment_style.nil? + errors.add(:payment_style, "can't be blank") + end + end + end + + def validate_uncollectables + if user.uncollectables.count > 0 + errors.add(:user, 'have unpaid lessons.') + end + end + + def school_owned? + !!school + end + + def self.book_packaged_test_drive(user, teacher, description, test_drive_package_choice) + book_test_drive(user, teacher, LessonBookingSlot.packaged_slots, description, test_drive_package_choice) + end + def self.book_free(user, teacher, lesson_booking_slots, description) + self.book(user, teacher, LessonBooking::LESSON_TYPE_FREE, lesson_booking_slots, false, 30, PAYMENT_STYLE_ELSEWHERE, description) + end + + def self.book_test_drive(user, teacher, lesson_booking_slots, description, test_drive_package_choice = nil) + self.book(user, teacher, LessonBooking::LESSON_TYPE_TEST_DRIVE, lesson_booking_slots, false, 30, PAYMENT_STYLE_ELSEWHERE, description, test_drive_package_choice) + end + + def self.book_normal(user, teacher, lesson_booking_slots, description, recurring, payment_style, lesson_length) + self.book(user, teacher, LessonBooking::LESSON_TYPE_PAID, lesson_booking_slots, recurring, lesson_length, payment_style, description) + end + + def self.book(user, teacher, lesson_type, lesson_booking_slots, recurring, lesson_length, payment_style, description, test_drive_package_choice = nil) + + lesson_booking = nil + LessonBooking.transaction do + + lesson_booking = LessonBooking.new + lesson_booking.user = user + lesson_booking.card_presumed_ok = user.has_stored_credit_card? + lesson_booking.sent_notices = false + lesson_booking.teacher = teacher + lesson_booking.lesson_type = lesson_type + lesson_booking.recurring = recurring + lesson_booking.lesson_length = lesson_length + lesson_booking.payment_style = payment_style + lesson_booking.description = description + lesson_booking.status = STATUS_REQUESTED + lesson_booking.test_drive_package_choice = test_drive_package_choice + if lesson_booking.teacher && lesson_booking.teacher.teacher.school + lesson_booking.school = lesson_booking.teacher.teacher.school + end + + if user + lesson_booking.same_school = !!(lesson_booking.school && user.school && (lesson_booking.school.id == user.school.id)) + else + lesson_booking.same_school = false + end + + # two-way association slots, for before_validation loic in slot to work + lesson_booking.lesson_booking_slots = lesson_booking_slots + lesson_booking_slots.each do |slot| + slot.lesson_booking = lesson_booking + slot.message = description + end if lesson_booking_slots + + if lesson_booking.save + description = '' if description.nil? + msg = ChatMessage.create(user, lesson_booking.lesson_sessions[0], description, ChatMessage::CHANNEL_LESSON, nil, teacher, lesson_booking.lesson_sessions[0], 'Lesson Requested') + end + end + lesson_booking + end + + def self.unprocessed(current_user) + LessonBooking.where(user_id: current_user.id).where(card_presumed_ok: false).where('school_id IS NULL') + end + + def self.requested(current_user) + LessonBooking.where(user_id: current_user.id).where(status: STATUS_REQUESTED) + end + + def school_on_school? + same_school + end + + def school_and_teacher + if school && school.scheduling_comm? + [school.communication_email, teacher.email] + else + [teacher.email] + end + end + + def school_and_teacher_ids + if school && school.scheduling_comm? + [school.owner.id, teacher.id] + else + [teacher.id] + end + end + + + def school_over_teacher + if school && school.scheduling_comm? + [school.communication_email] + else + [teacher.email] + end + end + + def school_over_teacher_ids + if school && school.scheduling_comm? + [school.owner.id] + else + [teacher.id] + end + end + + def self.find_bookings_needing_sessions(minimum_start_time) + MusicSession.select([:lesson_booking_id]).joins(:lesson_session => :lesson_booking).where("lesson_bookings.active = true").where('lesson_bookings.recurring = true').where("scheduled_start is not null").where("scheduled_start > ?", minimum_start_time).group(:lesson_booking_id).having('count(lesson_booking_id) < 2') + end + + # check for any recurring sessions where there are not at least 2 sessions into the future. If not, we need to make sure they get made + def self.hourly_check + schedule_upcoming_lessons + bill_monthlies + end + + def self.bill_monthlies + now = Time.now + billable_monthlies(now).each do |lesson_booking| + lesson_booking.bill_monthly(now) + end + + today = now.to_date + seven_days_in_future = today + 7 + + + is_different_month = seven_days_in_future.month != today.month + if is_different_month + next_month = seven_days_in_future.to_time + billable_monthlies(next_month).each do |lesson_booking| + lesson_booking.bill_monthly(next_month) + end + end + end + + def self.billable_monthlies(now) + current_month_first_day = Date.new(now.year, now.month, 1) + current_month_last_day = Date.new(now.year, now.month, -1) + #next_month_last_day = now.month == 12 ? Date.new(now.year + 1, 1, -1) : Date.new(now.year, now.month + 1, -1) + + LessonBooking + .joins(:lesson_sessions => :music_session) + .joins("LEFT JOIN lesson_package_purchases ON (lesson_package_purchases.lesson_booking_id = lesson_bookings.id AND (lesson_package_purchases.year = #{current_month_first_day.year} AND lesson_package_purchases.month = #{current_month_first_day.month}))") + .where("lesson_package_purchases.id IS NULL OR (lesson_package_purchases.id IS NOT NULL AND lesson_package_purchases.post_processed = false)") + .where(payment_style: PAYMENT_STYLE_MONTHLY) + .where(same_school: false) + .active + .where('music_sessions.scheduled_start >= ?', current_month_first_day) + .where('music_sessions.scheduled_start <= ?', current_month_last_day).uniq + +=begin + today = now.to_date + seven_days_in_future = today + 7 + + is_different_month = seven_days_in_future.month != today.month + if is_different_month + condition = "(((lesson_package_purchases.year = #{current_month_first_day.year} AND lesson_package_purchases.month = #{current_month_first_day.month}) AND ( (EXTRACT(YEAR FROM lesson_sessions.created_at)) = #{current_month_first_day.year} AND (EXTRACT(MONTH FROM lesson_sessions.created_at)) = #{current_month_first_day.month} ) ) + OR ((lesson_package_purchases.year = #{seven_days_in_future.year} AND lesson_package_purchases.month = #{seven_days_in_future.month}) AND ( (EXTRACT(YEAR FROM lesson_sessions.created_at)) = #{seven_days_in_future.year} AND (EXTRACT(MONTH FROM lesson_sessions.created_at)) = #{seven_days_in_future.month} ) ) )" + else + condition = "((lesson_package_purchases.year = #{current_month_first_day.year} AND lesson_package_purchases.month = #{current_month_first_day.month}) AND ( (EXTRACT(YEAR FROM lesson_sessions.created_at)) = #{current_month_first_day.year} AND (EXTRACT(MONTH FROM lesson_sessions.created_at)) = #{current_month_first_day.month} ) )" + end + + + # .where("(lesson_package_purchases.year = #{current_month_first_day.year} AND lesson_package_purchases.month = #{current_month_first_day.month}) OR (lesson_package_purchases.year = #{next_month_last_day.year} AND lesson_package_purchases.month = #{next_month_last_day.month})") + + # find any monthly-billed bookings that have a session coming up within 7 days, and if so, attempt to bill them + LessonBooking + .joins(:lesson_sessions) + .joins("LEFT JOIN lesson_package_purchases ON (lesson_package_purchases.lesson_booking_id = lesson_bookings.id AND #{condition})") + .where("lesson_package_purchases.id IS NULL OR (lesson_package_purchases.id IS NOT NULL AND lesson_package_purchases.post_processed = false)") + .where(payment_style: PAYMENT_STYLE_MONTHLY) + .where(status: STATUS_APPROVED) + .where('lesson_sessions.created_at >= ?', current_month_first_day) + .where('lesson_sessions.created_at <= ?', seven_days_in_future).uniq + +=end + end + + def self.bookings(student, teacher, since_at = nil) + bookings = LessonBooking.where(user_id: student.id, teacher_id: teacher.id) + + if since_at + bookings = bookings.where('created_at >= ?', since_at) + end + + bookings + end + + def self.not_failed + + end + def self.engaged_bookings(student, teacher, since_at = nil) + bookings = bookings(student, teacher, since_at) + bookings.engaged_or_successful + end + + def bill_monthly(now) + LessonBooking.transaction do + self.lock! + + current_month = Date.new(now.year, now.month, 1) + + bill_for_month(current_month) + + today = now.to_date + seven_days_in_future = today + 7 + is_different_month = seven_days_in_future.month != today.month + if is_different_month + bill_for_month(seven_days_in_future) + end + end + end + + def bill_for_month(day_in_month) + # try to find lesson package purchase for this month, and last month, and see if they need processing + current_month_purchase = lesson_package_purchases.where(lesson_booking_id: self.id, user_id: student.id, year: day_in_month.year, month: day_in_month.month).first + if current_month_purchase.nil? + current_month_purchase = LessonPackagePurchase.create(user, self, lesson_package_type, day_in_month.year, day_in_month.month) + end + current_month_purchase.bill_monthly + end + + def suspend! + # when this is called, the calling code sends out a email to let the student and teacher know (it feels unnatural it's not here, though) + self.status = STATUS_SUSPENDED + self.active = false + if self.save + future_sessions.each do |lesson_session| + LessonSession.find(lesson_session.id).suspend! + end + end + end + + def unsuspend! + if self.status == STATUS_SUSPENDED + self.status = STATUS_APPROVED + self.active = true + if self.save + future_sessions.each do |lesson_session| + LessonSession.find(lesson_session.id).unsuspend! + end + end + end + end + + def future_sessions + lesson_sessions.joins(:music_session).where('scheduled_start > ?', Time.now).order(:created_at) + end + + def self.schedule_upcoming_lessons + minimum_start_time = (Time.now + APP_CONFIG.minimum_lesson_booking_hrs * 60*60) + + lesson_bookings = find_bookings_needing_sessions(minimum_start_time) + + lesson_bookings.each do |data| + lesson_booking = LessonBooking.find(data["lesson_booking_id"]) + lesson_booking.sync_lessons + end + end + + + def scheduling_email + school_scheduling_comm? ? school.communication_email : teacher.email + end + + # when you need to email potentially both school and teacher for same email + def teacher_school_emails + if school_comm? + [school.communication_email, teacher.email] + else + [teacher.email] + end + + end + + def home_url + APP_CONFIG.external_root_url + "/client#/jamclass" + end + + def web_url + APP_CONFIG.external_root_url + "/client#/jamclass/lesson-booking/" + id + end + + def update_payment_url + APP_CONFIG.external_root_url + "/client#/jamclass/update-payment" + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/lesson_bookings/" + id + end + + private + def school_scheduling_comm? + school ? school.school_comm? : false + end + end +end diff --git a/ruby/lib/jam_ruby/models/lesson_booking_slot.rb b/ruby/lib/jam_ruby/models/lesson_booking_slot.rb new file mode 100644 index 000000000..d9d38ca47 --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_booking_slot.rb @@ -0,0 +1,284 @@ +# represenst the type of lesson package +module JamRuby + class LessonBookingSlot < ActiveRecord::Base + + include HtmlSanitize + html_sanitize strict: [:message, :accept_message, :cancel_message] + + @@log = Logging.logger[LessonBookingSlot] + + belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking" + belongs_to :lesson_session, class_name: "JamRuby::LessonSession" + belongs_to :proposer, class_name: "JamRuby::User" + has_one :defaulted_booking, class_name: "JamRuby::LessonBooking", foreign_key: :default_slot_id, inverse_of: :default_slot + has_one :countered_booking, class_name: "JamRuby::LessonBooking", foreign_key: :counter_slot_id, inverse_of: :counter_slot + has_one :countered_lesson, class_name: "JamRuby::LessonSession", foreign_key: :counter_slot_id, inverse_of: :counter_slot + + SLOT_TYPE_SINGLE = 'single' + SLOT_TYPE_RECURRING = 'recurring' + + SLOT_TYPES = [SLOT_TYPE_SINGLE, SLOT_TYPE_RECURRING] + + validates :proposer, presence: true + validates :slot_type, inclusion: {in: SLOT_TYPES} + #validates :preferred_day + validates :day_of_week, numericality: {only_integer: true}, allow_blank: true # 0 = sunday - 6 = saturday + validates :hour, numericality: {only_integer: true} + validates :minute, numericality: {only_integer: true} + validates :timezone, presence: true # example: 'America/New_York' + validates :update_all, inclusion: {in: [true, false]} + + validate :validate_slot_type + validate :validate_slot_minimum_time, on: :create + validate :validate_proposer + before_validation :before_validation + + def is_recurring? + slot_type == SLOT_TYPE_RECURRING + end + def before_validation + if proposer.nil? + self.proposer = container.student + end + end + + def container + if lesson_booking + lesson_booking + else + lesson_session + end + end + + def is_teacher_created? + self.proposer == container.teacher + end + + def is_student_created? + !is_teacher_created? + end + + def is_teacher_approved? + !is_teacher_created? + end + + def recipient + if is_teacher_created? + container.student + else + container.teacher + end + end + + def create_minimum_booking_time + (Time.now + APP_CONFIG.minimum_lesson_booking_hrs * 60 * 60) + end + + # create a canned slot for a TestDrivePackage. The most important thing here is that it expires in 30 days + def self.packaged_slots + slot = LessonBookingSlot.new + slot.from_package = true + slot.preferred_day = Date.today + 30 + slot.slot_type = LessonBookingSlot::SLOT_TYPE_SINGLE + slot.hour = 1 + slot.minute = 0 + slot.timezone = 'America/Chicago' + [slot] + end + + def scheduled_times(needed_sessions, minimum_start_time) + + #puts "NEEDED SESSIONS #{needed_sessions} #{minimum_start_time}" + times = [] + week_offset = 0 + + needed_sessions.times do |i| + candidate = scheduled_time(i + week_offset) + + #puts "#{i}: candidate #{candidate} week_offset:#{week_offset}" + #puts "DAY_OF_WEEK #{day_of_week}" + if day_of_week && candidate <= minimum_start_time + # move it up a week + week_offset += 1 + candidate = scheduled_time(i + week_offset) + + #puts "retry #1 #{candidate}" + # sanity check + if candidate <= minimum_start_time + week_offset += 1 + candidate = scheduled_time(i + week_offset) + + #puts "retry #2 #{candidate}" + if candidate <= minimum_start_time + + week_offset += 1 + candidate = scheduled_time(i + week_offset) + + #puts "retry #3 #{candidate}" + if candidate <= minimum_start_time + + week_offset += 1 + candidate = scheduled_time(i + week_offset) + + #puts "retry #4 #{candidate}" + if candidate <= minimum_start_time + raise "candidate time less than minimum start time even after scoot: #{lesson_booking.id} #{self.id}" + end + end + end + + end + end + times << candidate + end + + times + end + + def next_day + date = Date.today + date += ((day_of_week - date.wday) % 7).abs + end + + # weeks is the number of weeks in the future to compute the time for + def scheduled_time(weeks) + + # get the timezone of the slot, so we can compute times + tz = TZInfo::Timezone.get(timezone) + + if preferred_day + time = tz.local_to_utc(Time.new(preferred_day.year, preferred_day.month, preferred_day.day, hour, minute, 0)) + else + adjusted = next_day + (weeks * 7) + # day of the week adjustment + time = tz.local_to_utc(Time.new(adjusted.year, adjusted.month, adjusted.day, hour, minute, 0)) + end + + time + end + + def lesson_length + safe_lesson_booking.lesson_length + end + + def safe_lesson_booking + found = lesson_booking + found ||= lesson_session.lesson_booking + end + + def pretty_scheduled_start(with_timezone = true) + + start_time = scheduled_time(0) + + begin + tz = TZInfo::Timezone.get(timezone) + rescue Exception => e + @@log.error("unable to find timezone=#{tz_identifier}, e=#{e}") + end + + if tz + begin + start_time = tz.utc_to_local(start_time) + rescue Exception => e + @@log.error("unable to convert #{scheduled_start} to #{tz}, e=#{e}") + puts "unable to convert #{e}" + end + end + + + duration = lesson_length * 60 # convert from minutes to seconds + end_time = start_time + duration + if with_timezone + "#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} (#{tz.pretty_name})" + else + "#{start_time.strftime("%A, %B %e")} - #{start_time.strftime("%l:%M%P").strip}" + end + end + + def pretty_timezone + begin + tz = TZInfo::Timezone.get(timezone) + tz.pretty_name + rescue Exception => e + @@log.error("unable to find timezone=#{tz_identifier}, e=#{e}") + 'UTC' + end + end + + def pretty_start_time(with_timezone = true) + + start_time = scheduled_time(0) + + begin + tz = TZInfo::Timezone.get(timezone) + rescue Exception => e + @@log.error("unable to find timezone=#{tz_identifier}, e=#{e}") + end + + if tz + begin + start_time = tz.utc_to_local(start_time) + rescue Exception => e + @@log.error("unable to convert #{scheduled_start} to #{tz}, e=#{e}") + puts "unable to convert #{e}" + end + end + + duration = lesson_length * 60 # convert from minutes to seconds + end_time = start_time + duration + if with_timezone + "#{start_time.strftime("%a, %b %e")} at #{start_time.strftime("%l:%M%P").strip} #{tz.pretty_name}" + else + "#{start_time.strftime("%a, %b %e")} at #{start_time.strftime("%l:%M%P").strip}" + end + end + + def validate_proposer + if proposer && (proposer != container.student && proposer != container.teacher) + errors.add(:proposer, "must be either the student or teacher") + end + end + + def validate_slot_type + if slot_type == SLOT_TYPE_SINGLE + if preferred_day.nil? + errors.add(:preferred_day, "must be specified") + end + end + + if slot_type == SLOT_TYPE_RECURRING + if day_of_week.nil? + errors.add(:day_of_week, "must be specified") + end + end + end + + def validate_slot_minimum_time + + # this code will fail miserably if the slot is malformed + if errors.any? + return + end + + if is_teacher_created? + return # the thinking is that a teacher can propose much tighter to the time; since they only counter; maybe they talked to the student + end + + # + # minimum_start_time = create_minimum_booking_time + minimum_start_time = Time.now + + if day_of_week + # this is recurring; it will sort itself out + else + time = scheduled_time(0) + + if time <= minimum_start_time + #errors.add(:base, "must be at least #{APP_CONFIG.minimum_lesson_booking_hrs} hours in the future") + errors.add(:preferred_day, "can not be in the past") + end + end + + end + end +end diff --git a/ruby/lib/jam_ruby/models/lesson_package_purchase.rb b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb new file mode 100644 index 000000000..a38976b93 --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb @@ -0,0 +1,175 @@ +# represents the purchase of a LessonPackage +module JamRuby + class LessonPackagePurchase < ActiveRecord::Base + + @@log = Logging.logger[LessonPackagePurchase] + + delegate :sent_billing_notices, :last_billing_attempt_at, :billing_attempts, :billing_should_retry, :billed, :billed_at, :billing_error_detail, :billing_error_reason, :is_card_declined?, :is_card_expired?, :last_billed_at_date, :sent_billing_notices, to: :lesson_payment_charge + delegate :test_drive_count, to: :lesson_package_type + + # who purchased the lesson package? + belongs_to :user, class_name: "JamRuby::User", :foreign_key => "user_id", inverse_of: :lesson_purchases + belongs_to :lesson_package_type, class_name: "JamRuby::LessonPackageType" + belongs_to :teacher, class_name: "JamRuby::User" + belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking" + belongs_to :lesson_payment_charge, class_name: "JamRuby::LessonPaymentCharge", foreign_key: :charge_id + has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution" + + has_one :sale_line_item, class_name: "JamRuby::SaleLineItem" + + validates :user, presence: true + validates :lesson_package_type, presence: true + validates :price, presence: true + validate :validate_test_drive, on: :create + + after_create :add_test_drives + after_create :create_charge + + def validate_test_drive + if user + if lesson_package_type.is_test_drive? && !user.can_buy_test_drive? + errors.add(:user, "can not buy test drive right now because you have already purchased it within the last year") + end + end + end + + def create_charge + if !school_on_school? && lesson_booking && lesson_booking.is_monthly_payment? + self.lesson_payment_charge = LessonPaymentCharge.new + lesson_payment_charge.user = user + lesson_payment_charge.amount_in_cents = 0 + lesson_payment_charge.fee_in_cents = 0 + lesson_payment_charge.lesson_package_purchase = self + lesson_payment_charge.save! + end + end + + def add_test_drives + if self.lesson_package_type.is_test_drive? + new_test_drives = user.remaining_test_drives + lesson_package_type.test_drive_count + User.where(id: user.id).update_all(remaining_test_drives: new_test_drives) + user.remaining_test_drives = new_test_drives + end + + end + + + def to_s + "#{name}" + end + + def name + lesson_package_type.sale_display + end + + def amount_charged + lesson_payment_charge.amount_in_cents / 100.0 + end + + def self.create(user, lesson_booking, lesson_package_type, year = nil, month = nil) + purchase = LessonPackagePurchase.new + purchase.user = user + purchase.lesson_booking = lesson_booking + purchase.teacher = lesson_booking.teacher if lesson_booking + + if year + purchase.year = year + purchase.month = month + purchase.recurring = true + + if lesson_booking && lesson_booking.requires_teacher_distribution?(purchase) + purchase.teacher_distribution = TeacherDistribution.create_for_lesson_package_purchase(purchase) + # price should always match the teacher_distribution, if there is one + purchase.price = purchase.teacher_distribution.amount_in_cents / 100 + end + else + purchase.recurring = false + end + + if lesson_booking + purchase.lesson_package_type = lesson_package_type ? lesson_package_type : lesson_booking.lesson_package_type + purchase.price = lesson_booking.booked_price if purchase.price.nil? + else + purchase.lesson_package_type = lesson_package_type + purchase.price = lesson_package_type.price if purchase.price.nil? + end + + purchase.save + purchase + end + + def price_in_cents + (price * 100).to_i + end + + def description(lesson_booking) + lesson_package_type.description(lesson_booking) + end + + def stripe_description(lesson_booking) + description(lesson_booking) + end + + def timed_description + "Lessons for the month of #{self.month_name} with #{self.lesson_booking.student.name}" + end + + def month_name + if recurring + Date.new(year, month, 1).strftime('%B') + else + 'non-monthly paid lesson' + end + end + + def student + user + end + + # test drive purchase doesn't have a tea + def school_on_school? + if teacher + teacher.teacher.school && student.school && (teacher.teacher.school.id == student.school.id) + else + false + end + end + + + def bill_monthly(force = false) + + if school_on_school? + raise "school-on-school: should not be here" + else + lesson_payment_charge.charge(force) + success = lesson_payment_charge.billed + end + + if success + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + end + + def is_card_declined? + billed == false && billing_error_reason == 'card_declined' + end + + def is_card_expired? + billed == false && billing_error_reason == 'card_expired' + end + + def last_billed_at_date + last_billing_attempt_at.strftime("%B %d, %Y") if last_billing_attempt_at + end + + + def update_payment_url + APP_CONFIG.external_root_url + "/client#/jamclass/update-payment" + end + end +end + diff --git a/ruby/lib/jam_ruby/models/lesson_package_type.rb b/ruby/lib/jam_ruby/models/lesson_package_type.rb new file mode 100644 index 000000000..196e67ccd --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_package_type.rb @@ -0,0 +1,133 @@ +# represenst the type of lesson package +module JamRuby + class LessonPackageType < ActiveRecord::Base + + @@log = Logging.logger[LessonPackageType] + + PRODUCT_TYPE = 'LessonPackageType' + + SINGLE_FREE = 'single-free' + TEST_DRIVE_4 = 'test-drive' + TEST_DRIVE_2 = 'test-drive-2' + TEST_DRIVE_1 = 'test-drive-1' + SINGLE = 'single' + + LESSON_PACKAGE_TYPES = + [ + SINGLE_FREE, + TEST_DRIVE_4, + TEST_DRIVE_2, + TEST_DRIVE_1, + SINGLE + ] + + has_many :user_desired_packages, class_name: "JamRuby::User", :foreign_key => "lesson_package_type_id", inverse_of: :desired_package + validates :name, presence: true + validates :description, presence: true + validates :price, presence: true + validates :package_type, presence: true, inclusion: {in: LESSON_PACKAGE_TYPES} + + def self.test_drive_package_ids + [TEST_DRIVE_4, TEST_DRIVE_2, TEST_DRIVE_1] + end + def self.monthly + LessonPackageType.find(MONTHLY) + end + + def self.single_free + LessonPackageType.find(SINGLE_FREE) + end + + def self.test_drive_4 + LessonPackageType.find(TEST_DRIVE_4) + end + + def self.test_drive_2 + LessonPackageType.find(TEST_DRIVE_2) + end + + def self.test_drive_1 + LessonPackageType.find(TEST_DRIVE_1) + end + + def self.single + LessonPackageType.find(SINGLE) + end + + def booked_price(lesson_booking) + if is_single_free? + 0 + elsif is_test_drive? + 10.00 + elsif is_normal? + lesson_booking.booked_price #teacher.teacher.booking_price(lesson_booking.lesson_length, lesson_booking.payment_style == LessonBooking::PAYMENT_STYLE_SINGLE) + end + end + + def self.package_for_test_drive_count(count) + LessonPackageType.find_by_package_type("test-drive-#{count}") + end + def test_drive_count + package_type["test-drive-".length, 1].to_i + end + + def description(lesson_booking) + if is_single_free? + "Single Free Lesson" + elsif is_test_drive? + "Test Drive (#{test_drive_count})" + elsif is_normal? + if lesson_booking.recurring + "Recurring #{lesson_booking.payment_style == LessonBooking::PAYMENT_STYLE_WEEKLY ? "Weekly" : "Monthly"} #{lesson_booking.lesson_length}m" + else + "Single #{lesson_booking.lesson_length}m lesson" + end + end + end + + def stripe_description(lesson_booking) + description(lesson_booking) + end + + def is_single_free? + id == SINGLE_FREE + end + + def is_test_drive? + id.start_with?('test-drive') + end + + def is_normal? + id == SINGLE + end + + + def sale_display + name + end + + def plan_code + if package_type == SINGLE_FREE + "lesson-package-single-free" + elsif package_type == 'test-drive-4' + "lesson-package-test-drive-4" + elsif package_type == TEST_DRIVE_2 + "lesson-package-test-drive-2" + elsif package_type == TEST_DRIVE_1 + "lesson-package-test-drive-1" + elsif package_type == SINGLE + "lesson-package-single" + else + raise "unknown lesson package type #{package_type}" + end + end + + def sales_region + 'Worldwide' + end + + def to_s + sale_display + end + end +end diff --git a/ruby/lib/jam_ruby/models/lesson_payment_charge.rb b/ruby/lib/jam_ruby/models/lesson_payment_charge.rb new file mode 100644 index 000000000..f72380912 --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_payment_charge.rb @@ -0,0 +1,101 @@ +module JamRuby + class LessonPaymentCharge < Charge + + has_one :lesson_session, class_name: "JamRuby::LessonSession", foreign_key: :charge_id + has_one :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase", foreign_key: :charge_id + + def max_retries + 5 + end + + def charged_user + user + end + + def resolve_target + if is_lesson? + lesson_session + else + lesson_package_purchase + end + end + def target + @target ||= resolve_target + end + + def lesson_booking + @lesson_booking ||= target.lesson_booking + end + + def student + charged_user + end + + def teacher + target.teacher + end + + def is_lesson? + !lesson_session.nil? + end + + def do_charge(force) + + if is_lesson? + result = Sale.purchase_lesson(student, lesson_booking, lesson_booking.lesson_package_type, lesson_session) + else + result = Sale.purchase_lesson(student, lesson_booking, lesson_booking.lesson_package_type, nil, lesson_package_purchase, force) + lesson_booking.unsuspend! if lesson_booking.is_suspended? + end + + stripe_charge = result[:stripe_charge] + + self.amount_in_cents = stripe_charge.amount + self.save(validate: false) + + # update teacher distribution, because it's now ready to be given to them! + + distribution = target.teacher_distribution + if distribution # not all lessons/payment charges have a distribution + distribution.ready = true + distribution.save(validate: false) + end + + stripe_charge + end + + def do_send_notices + if is_lesson? + UserMailer.student_lesson_normal_done(lesson_session).deliver_now + UserMailer.teacher_lesson_normal_done(lesson_session).deliver_now + else + UserMailer.student_lesson_monthly_charged(lesson_package_purchase).deliver_now + UserMailer.teacher_lesson_monthly_charged(lesson_package_purchase).deliver_now + end + end + + def do_send_unable_charge + if is_lesson? + UserMailer.student_unable_charge(lesson_session) + else + if !billing_should_retry + lesson_booking.suspend! + end + + UserMailer.student_unable_charge_monthly(lesson_package_purchase) + if lesson_booking.is_suspended? + # let the teacher know that we are having problems collecting from the student + UserMailer.teacher_unable_charge_monthly(lesson_package_purchase) + end + end + end + + def description + target.timed_description + end + + def expected_price_in_cents + target.lesson_booking.distribution_price_in_cents(target) + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/lesson_session.rb b/ruby/lib/jam_ruby/models/lesson_session.rb new file mode 100644 index 000000000..0f2853101 --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_session.rb @@ -0,0 +1,1001 @@ +# represenst the type of lesson package +module JamRuby + class LessonSession < ActiveRecord::Base + + include HtmlSanitize + html_sanitize strict: [:cancel_message] + + attr_accessor :accepting, :creating, :countering, :countering_flag, :autocanceling, :countered_slot, :countered_lesson, :canceling, :assigned_student + + + @@log = Logging.logger[LessonSession] + + delegate :sent_billing_notices, :last_billing_attempt_at, :billing_attempts, :billing_should_retry, :billed_at, :billing_error_detail, :billing_error_reason, :is_card_declined?, :is_card_expired?, :last_billed_at_date, :sent_billing_notices, to: :lesson_payment_charge, allow_nil: true + delegate :is_test_drive?, :is_single_free?, :is_normal?, :approved_before?, :is_active?, :recurring, :is_monthly_payment?, :school_on_school?, :scheduling_email, :teacher_school_emails, :school_and_teacher, :school_over_teacher, :school_and_teacher_ids, :school_over_teacher_ids, to: :lesson_booking + delegate :pretty_scheduled_start, to: :music_session + + + STATUS_REQUESTED = 'requested' + STATUS_CANCELED = 'canceled' + STATUS_MISSED = 'missed' + STATUS_COMPLETED = 'completed' + STATUS_APPROVED = 'approved' + STATUS_SUSPENDED = 'suspended' + STATUS_COUNTERED = 'countered' + STATUS_UNCONFIRMED = 'unconfirmed' + + STATUS_TYPES = [STATUS_REQUESTED, STATUS_CANCELED, STATUS_MISSED, STATUS_COMPLETED, STATUS_APPROVED, STATUS_SUSPENDED, STATUS_COUNTERED, STATUS_UNCONFIRMED] + + LESSON_TYPE_SINGLE = 'paid' + LESSON_TYPE_SINGLE_FREE = 'single-free' + LESSON_TYPE_TEST_DRIVE = 'test-drive' + LESSON_TYPES = [LESSON_TYPE_SINGLE, LESSON_TYPE_SINGLE_FREE, LESSON_TYPE_TEST_DRIVE] + + has_one :music_session, class_name: "JamRuby::MusicSession", :dependent => :destroy + belongs_to :teacher, class_name: "JamRuby::User", foreign_key: :teacher_id, inverse_of: :taught_lessons + belongs_to :canceler, class_name: "JamRuby::User", foreign_key: :canceler_id + belongs_to :counterer, class_name: "JamRuby::User", foreign_key: :counterer_id + belongs_to :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase" + belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking" + belongs_to :user, class_name: "JamRuby::User" + belongs_to :slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :slot_id, :dependent => :destroy + belongs_to :lesson_payment_charge, class_name: "JamRuby::LessonPaymentCharge", foreign_key: :charge_id + belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_lesson, :dependent => :destroy + has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution" + has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot" + has_many :notifications, :class_name => "JamRuby::Notification", :foreign_key => "lesson_session_id" + has_many :chat_messages, :class_name => "JamRuby::ChatMessage", :foreign_key => "lesson_session_id" + + + validates :duration, presence: true, numericality: {only_integer: true} + validates :lesson_booking, presence: true + validates :lesson_type, inclusion: {in: LESSON_TYPES} + validates :booked_price, presence: true + validates :status, presence: true, inclusion: {in: STATUS_TYPES} + validates :teacher_complete, inclusion: {in: [true, false]} + validates :student_complete, inclusion: {in: [true, false]} + validates :teacher_canceled, inclusion: {in: [true, false]} + validates :student_canceled, inclusion: {in: [true, false]} + validates :success, inclusion: {in: [true, false]} + validates :sent_notices, inclusion: {in: [true, false]} + validates :post_processed, inclusion: {in: [true, false]} + + validate :validate_creating, :if => :creating + validate :validate_countering, :if => :countering_flag + validate :validate_accepted, :if => :accepting + validate :validate_canceled, :if => :canceling + validate :validate_autocancel, :if => :autocanceling + + after_save :manage_slot_changes + after_create :create_charge + + scope :approved, -> { where(status: STATUS_APPROVED) } + scope :requested, -> { where(status: STATUS_REQUESTED) } + scope :canceled, -> { where(status: STATUS_CANCELED) } + scope :suspended, -> { where(status: STATUS_SUSPENDED) } + scope :completed, -> { where(status: STATUS_COMPLETED) } + scope :missed, -> { where(status: STATUS_MISSED) } + scope :upcoming, -> { joins(:music_session).where('music_sessions.scheduled_start > ?', Time.now) } + scope :past_cancel_window, -> { joins(:music_session).where('music_sessions.scheduled_start > ?', 24.hours.from_now) } + # show all requested/countered sessions where the student was the last to communicate + scope :slow_responses, -> { joins(:lesson_booking).where('lesson_sessions.status = ? OR lesson_sessions.status = ?', LessonSession::STATUS_REQUESTED, LessonSession::STATUS_COUNTERED) + .where('lesson_bookings.counterer_id IS NULL OR lesson_bookings.user_id = lesson_bookings.counterer_id') + .order('(COALESCE(lesson_bookings.countered_at, lesson_bookings.sent_notices_at)) ASC') } + scope :least_time_left, -> { joins(:lesson_booking, :music_session).where('lesson_sessions.status = ? OR lesson_sessions.status = ?', LessonSession::STATUS_REQUESTED, LessonSession::STATUS_COUNTERED) + .where('lesson_bookings.counterer_id IS NULL OR lesson_bookings.user_id = lesson_bookings.counterer_id') + .order('music_sessions.scheduled_start DESC') } + + def create_charge + if !school_on_school? && !is_test_drive? && !is_monthly_payment? + self.lesson_payment_charge = LessonPaymentCharge.new + lesson_payment_charge.user = @assigned_student + lesson_payment_charge.amount_in_cents = 0 + lesson_payment_charge.fee_in_cents = 0 + lesson_payment_charge.lesson_session = self + lesson_payment_charge.save! + end + end + + def manage_slot_changes + # if this slot changed, we need to update the time. But LessonBooking does this for us, for requested/accepted . + # TODO: what to do, what to do. + + end + + def suspend! + self.status = STATUS_SUSPENDED + self.save + end + + def unsuspend! + self.status = STATUS_APPROVED + self.save + end + + def music_session_id + music_session.id + end + + def self.hourly_check + auto_cancel + analyse_sessions + complete_sessions + remind_counters + end + + def self.minutely_check + upcoming_sessions_reminder + end + + def self.remind_counters + MusicSession.joins(lesson_session: :lesson_booking) + .where("lesson_bookings.sent_counter_reminder = false") + .where('lesson_sessions.status = ? OR lesson_sessions.status = ?', LessonSession::STATUS_REQUESTED, LessonSession::STATUS_COUNTERED) + .where("? > (COALESCE(lesson_bookings.countered_at, lesson_bookings.sent_notices_at)) + (INTERVAL '24 hours')", Time.now).each do |music_session| + lesson_session = music_session.lesson_session + if lesson_session.student_last_proposed? + UserMailer.teacher_counter_reminder(lesson_session).deliver! + else + UserMailer.student_counter_reminder(lesson_session).deliver! + end + lesson_session.lesson_booking.sent_counter_reminder = true + lesson_session.lesson_booking.save(validate: false) + end + end + + def self.auto_cancel + MusicSession.joins(lesson_session: :lesson_booking).where('lesson_sessions.status = ?', LessonSession::STATUS_REQUESTED).where("? > scheduled_start + (INTERVAL '1 minutes' * (duration))", Time.now).each do |music_session| + lesson_session = music_session.lesson_session + lesson_session.autocancel + end + end + + def self.analyse_sessions + MusicSession.joins(lesson_session: :lesson_booking).where('lesson_sessions.status = ?', LessonSession::STATUS_APPROVED).where("? > scheduled_start + (INTERVAL '1 minutes' * (duration))", Time.now).where('analysed = false').each do |music_session| + lesson_session = music_session.lesson_session + lesson_session.analyse + end + end + + def self.complete_sessions + # this will find any paid session (recurring monthly paid, recurring single paid, single paid) + MusicSession.joins(lesson_session: [:lesson_booking, :lesson_payment_charge]).where('lesson_sessions.status = ?', LessonSession::STATUS_COMPLETED).where("? > scheduled_start + (INTERVAL '1 minutes' * (duration))", Time.now).where('analysed = true').where('lesson_sessions.post_processed = false').where('billing_should_retry = true').each do |music_session| + lesson_session = music_session.lesson_session + lesson_session.session_completed + end + + # test drives don't have a lesson_payment_charge, so we don't join against them + MusicSession.joins(lesson_session: [:lesson_booking]).where('lesson_sessions.status = ?', LessonSession::STATUS_COMPLETED).where('lesson_sessions.lesson_type = ?', LESSON_TYPE_TEST_DRIVE).where("? > scheduled_start + (INTERVAL '1 minutes' * duration)", Time.now).where('analysed = true').where('lesson_sessions.post_processed = false').each do |music_session| + lession_session = music_session.lesson_session + lession_session.session_completed + end + end + + def self.upcoming_sessions_reminder + now = Time.now + half_hour_from_now = 30.minutes.from_now + if Time.zone + now = Time.zone.local_to_utc(now) + half_hour_from_now = Time.zone.local_to_utc(half_hour_from_now) + end + + MusicSession.joins(lesson_session: [:lesson_booking]).where('lesson_sessions.status = ?', LessonSession::STATUS_APPROVED).where('sent_starting_notice = false').where('(scheduled_start > ? and scheduled_start < ?)', now, half_hour_from_now).each do |music_session| + lession_session = music_session.lesson_session + lession_session.send_starting_notice + end + end + + def student_last_proposed? + counterer_id.nil? || counterer_id == student_id + end + + def analyse + if self.analysed + return + end + + analysis = LessonSessionAnalyser.analyse(self) + + self.analysis = LessonSession.analysis_to_json(analysis) + + if analysis[:reason] == LessonSessionAnalyser::SESSION_ONGOING + # extra protection against bad code somewhere + return + end + self.success = analysis[:bill] + self.analysed_at = Time.now + self.analysed = true + + self.status = STATUS_COMPLETED + + if success && lesson_booking.requires_teacher_distribution?(self) + self.teacher_distribution = TeacherDistribution.create_for_lesson(self) + end + + if self.save + # send out emails appropriate for this type of session + session_completed + end + end + + def billed + if lesson_booking.is_test_drive? + false + elsif lesson_payment_charge + lesson_payment_charge.billed + else + false + end + end + + def amount_charged + if lesson_payment_charge + lesson_payment_charge.amount_in_cents / 100.0 + else + 0.0 + end + end + + def self.analysis_to_json(analysis, preserve_object = false) + json = {} + + analysis.each do |k, v| + if v.is_a?(Array) + array = [] + v.each do |item| + if item.is_a?(Range) + array << {begin: item.begin, end: item.end} + else + raise "expected range" + end + end + json[k] = array + else + json[k] = v + end + end + if preserve_object + json + else + json.to_json + end + end + + def send_starting_notice + UserMailer.lesson_starting_soon_student(self).deliver_now + UserMailer.lesson_starting_soon_teacher(self).deliver_now + + self.sent_starting_notice = true + self.save(validate: false) + end + + def session_completed + LessonSession.transaction do + self.lock! + + if post_processed + # nothing to do. because this is an async job, it's possible this was queued up with another session_completed fired + return + end + + if lesson_booking.is_test_drive? + test_drive_completed + elsif lesson_booking.is_normal? + if lesson_booking.is_weekly_payment? || lesson_booking.is_monthly_payment? + recurring_completed + else + normal_lesson_completed + end + end + + end + end + + def bill_lesson + if school_on_school? + success = true + else + lesson_payment_charge.charge + success = lesson_payment_charge.billed + end + + if success + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + end + + def student_missed + analysed && !success && analysis_json["student_analysis"]["missed"] + end + + def teacher_missed + analysed && !success && analysis_json["teacher_analysis"]["missed"] + end + + def error_display + if analysed + if !success + if student_missed && teacher_missed + "both the student and teacher missed the lesson" + elsif teacher_missed + "the teacher missed the lesson" + elsif student_missed + "the student missed the lesson" + else + "unknown error reason" + end + + else + "because no error" + end + else + "not yet analysed" + end + + end + + + def test_drive_completed + + distribution = teacher_distribution + + if !sent_notices + if success + if distribution # not all lessons/payment charges have a distribution + distribution.ready = true + distribution.save(validate: false) + end + student.test_drive_succeeded(self) + else + student.test_drive_failed(self) + end + + self.lesson_booking.success = success + self.lesson_booking.status = STATUS_COMPLETED + self.lesson_booking.save(:validate => false) + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + end + + def recurring_completed + puts "RECURRING COMPLETED #{success}" + if success + + if lesson_booking.is_monthly_payment? + # monthly payments are handled at beginning of month; just poke with email, and move on + + if !sent_notices + # not in spec; just poke user and tell them we saw it was successfully completed + UserMailer.monthly_recurring_done(self).deliver_now + + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + else + bill_lesson + end + else + if lesson_booking.is_monthly_payment? + if !sent_notices + if !school_on_school? + # bad session; just poke user + UserMailer.monthly_recurring_no_bill(self).deliver_now + end + + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + + else + if !sent_notices + if !school_on_school? + # bad session; just poke user + UserMailer.student_lesson_normal_no_bill(self).deliver_now + end + + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + + end + end + end + + def normal_lesson_completed + if success + bill_lesson + else + if !sent_notices + if !school_on_school? + puts "STUDENT NO BILL SENT #{success}" + UserMailer.student_lesson_normal_no_bill(self).deliver_now + UserMailer.teacher_lesson_normal_no_bill(self).deliver_now + end + + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + end + + self.lesson_booking.success = success + self.lesson_booking.status = STATUS_COMPLETED + self.lesson_booking.save(:validate => false) + end + + def scheduled_start + if music_session + music_session.scheduled_start + else + raise "lesson session #{id} has no music session in scheduled_start" + end + + end + + def send_counter(countered_lesson, countered_slot) + if !lesson_booking.errors.any? + if countered_slot.is_teacher_created? + UserMailer.student_lesson_counter(countered_lesson, countered_slot).deliver_now + else + UserMailer.teacher_lesson_counter(countered_lesson, countered_slot).deliver_now + end + end + self.countering = false + end + + default_scope { order('lesson_sessions.created_at') } + + def is_requested? + status == STATUS_REQUESTED + end + + def is_canceled? + status == STATUS_CANCELED + end + + def is_completed? + status == STATUS_COMPLETED + end + + def is_approved? + status == STATUS_APPROVED + end + + def is_suspended? + status == STATUS_SUSPENDED + end + + def is_countered? + status == STATUS_COUNTERED + end + + def analysis_json + @parsed_analysis || analysis + end + + def validate_creating + if !is_requested? && !is_approved? + self.errors.add(:status, "is not valid for a new lesson session.") + end + + if is_approved? && lesson_booking && lesson_booking.is_test_drive? && lesson_package_purchase.nil? + self.errors.add(:lesson_package_purchase, "must be specified for a test drive purchase") + end + end + + def validate_countering + + if counter_slot.nil? + errors.add(:counter_slot, "must be specified") + elsif !approved_before? && (status == STATUS_REQUESTED || status == STATUS_COUNTERED) + if recurring && !counter_slot.update_all + errors.add(:counter_slot, "Only 'update all' counter-proposals are allowed for un-approved, recurring lessons") + end + + if recurring && !counter_slot.is_recurring? + errors.add(:counter_slot, "Only 'recurring' counter-proposals are allowed for un-approved, recurring lessons") + end + end + + self.countering_flag = false + end + + def validate_accepted + if self.status_was != STATUS_REQUESTED && self.status_was != STATUS_COUNTERED + self.errors.add(:status, "This session is already #{self.status_was}.") + end + + # if this is a single lesson (testdrive, paid), but the scheduled time is before now, then it's in the past, and we have to reject + if !recurring && self.slot.scheduled_time(0) <= Time.now + self.errors.add(:slot, "is in the past") + end + + if approved_before? + # only checking for this on 1st time through acceptance + if lesson_booking && lesson_booking.is_test_drive? && lesson_package_purchase.nil? + self.errors.add(:lesson_package_purchase, "must be specified for a test drive purchase") + end + end + + self.accepting = false + end + + def validate_autocancel + + if scheduled_start + (duration * 60) > Time.now + self.errors.add(:status, "This session is not in the past.") + end + + if self.status_was != STATUS_REQUESTED + self.errors.add(:status, "This session is #{self.status_was} and can not be autocanceled") + end + + self.autocanceling = false + end + + def validate_canceled + if !is_canceled? + self.errors.add(:status, "This session is already #{self.status}.") + end + + # check 24 hour window + if scheduled_start.to_i - Time.now.to_i < 24 * 60 * 60 + self.errors.add(:base, "This session is due to start within 24 hours and can not be canceled.") + end + + self.canceling = false + end + + def self.create(booking) + lesson_session = LessonSession.new + lesson_session.creating = true + lesson_session.duration = booking.lesson_length + lesson_session.lesson_type = booking.lesson_type + lesson_session.lesson_booking = booking + lesson_session.booked_price = booking.booked_price + lesson_session.teacher = booking.teacher + lesson_session.status = booking.status + lesson_session.slot = booking.default_slot + lesson_session.assigned_student = booking.student + lesson_session.user = booking.student + if booking.is_test_drive? && booking.student.remaining_test_drives > 0 + lesson_session.lesson_package_purchase = booking.student.most_recent_test_drive_purchase + end + lesson_session.save + + if lesson_session.errors.any? + puts "Lesson Session errors #{lesson_session.errors.inspect}" + end + lesson_session + end + + def student + music_session.creator + end + + def student_id + music_session.creator.id + end + + def self.index(user, params = {}) + limit = params[:per_page] + limit ||= 100 + limit = limit.to_i + + school_owner = user.owned_school && user.owned_school.scheduling_comm? + + is_teacher = !user.teacher.nil? + + query = LessonSession.unscoped.joins([:music_session, :lesson_booking]).joins(music_session: :creator) + #query = query.includes([:teacher, :music_session]) + query = query.includes([:music_session]) + query = query.order('music_sessions.scheduled_start DESC') + + + if params[:as_teacher].present? + if school_owner || params[:as_teacher] == true || params[:as_teacher] == "true" + + # if a school owner is viewing, grab his owned school ID. Otherwise use a teacher's school ID, if applicable + school_id = school_owner ? user.owned_school.id : (user.teacher ? user.teacher.school_id : nil) + + if school_id + school_extra = "OR (lesson_bookings.school_id = '#{school_id}')" + else + school_extra = '' + end + + if school_owner + extra_teacher= '' + if is_teacher + # if the school owner is a teacher, show his bookings too + extra_teacher = " OR lesson_sessions.teacher_id = '#{user.teacher.id}'" + end + query = query.where('lesson_sessions.teacher_id in (?)' + extra_teacher, user.owned_school.teachers.map { |t| t.user.id }) + query = query.where('lesson_sessions.status = ? OR lesson_sessions.status = ?', LessonSession::STATUS_REQUESTED, LessonSession::STATUS_COUNTERED) + else + # this is a normal teacher (not a school owner) + if school_id && user.teacher.school.scheduling_comm? + # the school wants to control scheduling communication. So hide sessions in the requested/countered status from them + query = query.where('lesson_sessions.status != ? AND lesson_sessions.status != ?', LessonSession::STATUS_REQUESTED, LessonSession::STATUS_COUNTERED) + end + query = query.where('lesson_sessions.teacher_id = ?', user.id) + end + + else + if user.school_id + school_extra = "OR (lesson_bookings.school_id = '#{user.school_id}')" + else + school_extra = '' + end + + + query = query.where('music_sessions.user_id = ?', user.id) + end + else + + if user.school_id + school_extra = "OR (lesson_bookings.school_id = '#{user.school_id}')" + else + school_extra = '' + end + + query = query.where('(lesson_sessions.teacher_id = ? or music_sessions.user_id = ?)', user.id, user.id) + end + + query = query.where('lesson_bookings.card_presumed_ok = true OR (music_sessions.user_id = ?) ' + school_extra, user.id) + + 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 update_scheduled_start(week_offset) + music_session.scheduled_start = slot.scheduled_time(week_offset) + music_session.save! + end + + # grabs the next available time that's after the present, to avoid times being scheduled in the past + def update_next_available_time(attempt = 0) + max_attempts = attempt + 10 + while attempt < max_attempts + test = slot.scheduled_time(attempt) + + if test >= Time.now + time = test + # valid time found! + break + end + attempt += 1 + end + + if time + music_session.scheduled_start = time + music_session.save + end + + time.nil? ? nil : attempt + end + + def school_owner_id + school = teacher.teacher.school + if school + school.owner.id + end + end + + def access?(user) + user.id == music_session.user_id || user.id == teacher.id || user.id == school_owner_id + end + + # teacher accepts the lesson + def accept(params) + response = self + LessonSession.transaction do + + message = params[:message] + slot = params[:slot] + accepter = params[:accepter] + raise "LessonBookingSlot" if slot.is_a?(LessonBookingSlot) + self.slot = slot = LessonBookingSlot.find(slot) + self.slot.accept_message = message + self.slot.save! + self.accepting = true + self.status = STATUS_APPROVED + + if !approved_before? + # 1st time this has ever been approved; there are other things we need to do + + if lesson_package_purchase.nil? && lesson_booking.is_test_drive? + self.lesson_package_purchase = student.most_recent_test_drive_purchase + end + + if self.save + # also let the lesson_booking know we got accepted + if !lesson_booking.accept(self, slot, accepter) + response = lesson_booking + raise ActiveRecord::Rollback + end + UserMailer.student_lesson_accepted(self, message, slot).deliver_now + UserMailer.teacher_lesson_accepted(self, message, slot).deliver_now + message = '' if message.nil? + msg = ChatMessage.create(teacher, nil, message, ChatMessage::CHANNEL_LESSON, nil, student, self, "Lesson Approved") + Notification.send_jamclass_invitation_teacher(music_session, teacher) + Notification.send_student_jamclass_invitation(music_session, student) + Notification.send_lesson_message('accept', self, true) + + else + @@log.error("unable to accept slot #{slot.id} for lesson #{self.id}") + puts("unable to accept slot #{slot.id} for lesson #{self.id}") + response = self + raise ActiveRecord::Rollback + end + else + # this implies a new slot has been countered, and now approved + + if self.save + if slot.update_all + if !lesson_booking.accept(self, slot, accepter) + response = lesson_booking + raise ActiveRecord::Rollback + end + message = '' if message.nil? + msg = ChatMessage.create(slot.proposer, nil, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, self, "All Lesson Times Updated") + Notification.send_lesson_message('accept', self, true) # TODO: this isn't quite an 'accept' + UserMailer.student_lesson_update_all(self, message, slot).deliver_now + UserMailer.teacher_lesson_update_all(self, message, slot).deliver_now + else + # nothing to do with the original booking (since we are not changing all times), so we update just ourself + time = update_next_available_time # XXX: week offset as 0? This *could* still be in the past. But the user is approving it. So do we just trust them and get out of their way? + + if time.nil? + @@log.error("unable to accept slot #{slot.id} for lesson #{self.id} because it's in the past") + puts("unable to accept slot #{slot.id} for lesson #{self.id} because it's in the past") + raise ActiveRecord::Rollback + end + message = '' if message.nil? + msg = ChatMessage.create(slot.proposer, nil, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, self, "Lesson Updated Time Approved") + UserMailer.student_lesson_accepted(self, message, slot).deliver_now + UserMailer.teacher_lesson_accepted(self, message, slot).deliver_now + end + else + @@log.error("unable to accept slot #{slot.id} for lesson #{self.id} #{errors.inspect}") + puts("unable to accept slot #{slot.id} for lesson #{self.id} #{errors.inspect}") + response = self + raise ActiveRecord::Rollback + end + end + end + response + end + + def counter(params) + response = self + LessonSession.transaction do + proposer = params[:proposer] + slot = params[:slot] + message = params[:message] + + update_all = slot.update_all || !lesson_booking.recurring + self.countering = true + self.countering_flag = true + slot.proposer = proposer + slot.lesson_session = self + slot.message = message + self.counterer = proposer + self.countered_at = Time.now + self.lesson_booking_slots << slot + self.countered_slot = slot + self.countered_lesson = self + self.status = STATUS_COUNTERED + #if !update_all + self.counter_slot = slot + #end + if self.save + puts "SAVING OK" + #if update_all && !lesson_booking.counter(self, proposer, slot) + if !lesson_booking.counter(self, proposer, slot) + response = lesson_booking + puts "ROLLBACK" + raise ActiveRecord::Rollback + end + else + response = self + raise ActiveRecord::Rollback + end + + send_counter(@countered_lesson, @countered_slot) + message = '' if message.nil? + msg = ChatMessage.create(slot.proposer, music_session, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, self, "New Time Proposed") + Notification.send_lesson_message('counter', self, slot.is_teacher_created?) + end + + response + end + + def cancel_lesson(canceler, message) + canceled_by_student = canceler == student + self.status = STATUS_CANCELED + self.cancel_message = message + self.canceler = canceler + self.canceling = true + + if canceled_by_student + self.student_canceled = true + self.student_canceled_at = Time.now + self.student_canceled_reason = message + self.student_short_canceled = 24.hours.from_now > scheduled_start + else + self.teacher_canceled = true + self.teacher_canceled_at = Time.now + self.teacher_canceled_reason = message + self.teacher_short_canceled = 24.hours.from_now > scheduled_start + end + end + + # canceled by the system because it is requested, and the end time has gone by + def autocancel + response = self + LessonSession.transaction do + self.autocanceling = true + self.status = LessonSession::STATUS_UNCONFIRMED + if self.save + if !lesson_booking.recurring + response = lesson_booking.autocancel + if response.errors.any? + raise ActiveRecord::Rollback + end + end + else + raise ActiveRecord::Rollback + end + + msg = ChatMessage.create(teacher, nil, '', ChatMessage::CHANNEL_LESSON, nil, student, self, "Lesson Timeout") + msg = ChatMessage.create(student, nil, '', ChatMessage::CHANNEL_LESSON, nil, teacher, self, "Lesson Timeout") + end + end + + # teacher accepts the lesson + def cancel(params) + response = self + LessonSession.transaction do + + self.status = LessonSession::STATUS_CANCELED + canceler = params[:canceler] + canceled_by_student = canceler == student + other = canceled_by_student ? teacher : student + message = params[:message] + message = '' if message.nil? + + if lesson_booking.recurring + update_all = params[:update_all] + else + update_all = true + end + + if lesson_booking.is_test_drive? + student.test_drive_declined(self) + end + + if update_all + response = lesson_booking.cancel(canceler, other, message) + if response.errors.any? + raise ActiveRecord::Rollback + end + + if !save + response = self + raise ActiveRecord::Rollback + end + else + cancel_lesson(canceler, message) + if !save + response = self + raise ActiveRecord::Rollback + end + + msg = ChatMessage.create(canceler, nil, message, ChatMessage::CHANNEL_LESSON, nil, other, self, "Lesson Canceled") + Notification.send_lesson_message('canceled', self, false) + Notification.send_lesson_message('canceled', self, true) + UserMailer.student_lesson_canceled(self, message).deliver_now + UserMailer.teacher_lesson_canceled(self, message).deliver_now + end + end + + response + end + + def description(lesson_booking) + lesson_booking.lesson_package_type.description(lesson_booking) + end + + def timed_description + if is_test_drive? + "TestDrive session with #{self.lesson_booking.student.name} on #{self.scheduled_start.to_date.strftime('%B %d, %Y')}" + else + if self.lesson_booking.is_monthly_payment? + "Monthly Lesson with #{self.lesson_booking.student.name} on #{self.scheduled_start.to_date.strftime('%B %d, %Y')}" + else + "Lesson with #{self.lesson_booking.student.name} on #{self.scheduled_start.to_date.strftime('%B %d, %Y')}" + end + end + end + + def display_type + if is_test_drive? + 'TestDrive' + elsif recurring + 'Recurring' + else + 'Single' + end + end + + def last_student_comm_date + counterer_id == user_id ? countered_at : sent_notices_at + end + + def days_no_response + date = last_student_comm_date + if date.nil? + '?' + else + (Time.now - last_student_comm_date) / (60 * 60 * 24) + end + + + end + + def stripe_description(lesson_booking) + description(lesson_booking) + end + + def home_url + APP_CONFIG.external_root_url + "/client#/jamclass" + end + + def web_url + APP_CONFIG.external_root_url + "/client#/jamclass/lesson-booking/" + id + end + + def update_payment_url + APP_CONFIG.external_root_url + "/client#/jamclass/update-payment" + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/lesson_sessions/" + id + end + + def chat_url + APP_CONFIG.external_root_url + "/client#/jamclass/chat-dialog/d1=lesson_" + id + end + end +end diff --git a/ruby/lib/jam_ruby/models/lesson_session_analyser.rb b/ruby/lib/jam_ruby/models/lesson_session_analyser.rb new file mode 100644 index 000000000..8d3ac0b03 --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_session_analyser.rb @@ -0,0 +1,343 @@ +module JamRuby + class LessonSessionAnalyser + + SUCCESS = 'success' + SESSION_ONGOING = 'session_ongoing' + THRESHOLD_MET = 'threshold_met' + WAITED_CORRECTLY = 'waited_correctly' + MINIMUM_TIME_MET = 'minimum_time_met' # for a teacher primarily; they waited around for the student sufficiently + MINIMUM_TIME_NOT_MET = 'mininum_time_not_met' + LATE_CANCELLATION = 'late_cancellation' + + TEACHER_FAULT = 'teacher_fault' + STUDENT_FAULT = 'student_fault' + BOTH_FAULT = 'both_fault' + + STUDENT_NOT_THERE_WHEN_JOINED = 'student_not_there_when_joined' + JOINED_LATE = 'did_not_join_on_time' + NO_SHOW = 'no_show' + NEITHER_SHOW = 'neither_show' + + + # what are the potential results? + + # bill: true/false + + # teacher: 'no_show' + # teacher: 'late' + # teacher: 'early_leave' + # teacher: 'waited_correctly' + # teacher: 'late_cancellation' + + # student: 'no_show' + # student: 'late' + # student: 'early_leave' + # student: 'minimum_time_not_met' + # student: 'threshold_met' + + + # reason: 'session_ongoing' + # reason: 'success' + # reason: 'student_fault' + # reason: 'teacher_fault' + # reason: 'both_fault' + + + def self.analyse(lesson_session, force = false) + reason = nil + teacher = nil + student = nil + bill = false + + music_session = lesson_session.music_session + + student_histories = MusicSessionUserHistory.where(music_session_id: music_session.id, user_id: lesson_session.student.id) + teacher_histories = MusicSessionUserHistory.where(music_session_id: music_session.id, user_id: lesson_session.teacher.id) + + # create ranges from music session user history + all_student_ranges = time_ranges(student_histories) + all_teacher_ranges = time_ranges(teacher_histories) + + # flatten ranges into non-overlapping ranges to simplifly logic + student_ranges = merge_overlapping_ranges(all_student_ranges) + teacher_ranges = merge_overlapping_ranges(all_teacher_ranges) + + intersecting = intersecting_ranges(student_ranges, teacher_ranges) + student_analysis = analyse_intersection(lesson_session, student_ranges) + teacher_analysis = analyse_intersection(lesson_session, teacher_ranges) + together_analysis = analyse_intersection(lesson_session, intersecting) + + # spec: https://jamkazam.atlassian.net/wiki/display/PS/Product+Specification+-+JamClass#ProductSpecification-JamClass-TeacherReceives&RespondstoLessonBookingRequest + + if !force && !((music_session.scheduled_start + (lesson_session.duration * 60)) < Time.now) + reason = SESSION_ONGOING + bill = false + else + #if lesson_session.is_canceled? && lesson_session.canceled_by_teacher? && lesson_session.canceled_late? + # # If the lesson was cancelled less than 24 hours before the start time by the teacher, then we do not bill the student. + # teacher = LATE_CANCELLATION + # bill = false + #elsif lesson_session.is_canceled? && lesson_session.canceled_by_student? && lesson_session.canceled_late? + # # If the lesson was cancelled less than 24 hours before the start time by the student (if that is even possible, I can’t remember now), then we do bill the student. + # student = LATE_CANCELLATION + # bill = true + if together_analysis[:session_time] / 60 > APP_CONFIG.lesson_together_threshold_minutes + bill = true + reason = SUCCESS + elsif teacher_analysis[:joined_on_time] && teacher_analysis[:waited_correctly] + # if the teacher was present in the session within the first 5 minutes of the scheduled start time and stayed in the session for 10 minutes; + # and if either: + + if student_analysis[:no_show] + # the student no-showed entirely, then we bill the student. + student = NO_SHOW + bill = true + + elsif student_analysis[:joined_late] + # the student joined the lesson more than 10 minutes after the teacher did, regardless of whether the teacher was still in the lesson session at that point; then we bill the student + student = JOINED_LATE + bill = true + end + else + if teacher_analysis[:no_show] + teacher = NO_SHOW + elsif !teacher_analysis[:joined_on_time] + teacher = JOINED_LATE + elsif !teacher_analysis[:waited_correctly] + teacher = MINIMUM_TIME_NOT_MET + end + end + + + end + + if reason.nil? + if student + reason = STUDENT_FAULT + elsif teacher + reason = TEACHER_FAULT + else + reason = NEITHER_SHOW + end + end + + + { + reason: reason, + teacher: teacher, + student: student, + bill: bill, + student_ranges: student_ranges, + teacher_ranges: teacher_ranges, + intersecting: intersecting, + student_analysis: student_analysis, + teacher_analysis: teacher_analysis, + together_analysis: together_analysis, + } + end + + def self.annotate_timeline(lesson_session, analysis, ranges) + start = lesson_session.scheduled_start + end + + def self.intersecting_ranges(ranges_a, ranges_b) + intersections = [] + ranges_a.each do |range_a| + ranges_b.each do |range_b| + intersection = intersect(range_a, range_b) + intersections << intersection if intersection + end + end + + merge_overlapping_ranges(intersections) + end + + # needs to add to joined_on_time + # joined_on_time bool + # waited_correctly bool + # no_show bool + # joined_late bool + # minimum_time_met bool + # present_at_end bool + + + def self.analyse_intersection(lesson_session, ranges) + # be sure to call .to_time on any ActiveRecord time, because we get a ton of deprecation warninsg about Time#succ if you use ActiveSupport:: TimeZone + start = lesson_session.scheduled_start.to_time + planned_duration_seconds = lesson_session.duration * 60 + end_time = start + planned_duration_seconds + + join_start_boundary_begin = start + join_start_boundary_end = start + (APP_CONFIG.lesson_join_time_window_minutes * 60) + + wait_boundary_begin = start + wait_boundary_end = start + (APP_CONFIG.lesson_wait_time_window_minutes * 60) + + initial_join_window = Range.new(join_start_boundary_begin, join_start_boundary_end) + initial_wait_window = Range.new(wait_boundary_begin, wait_boundary_end) + session_window = Range.new(start, end_time) + + # let's see how much time they spent together, irrespective of scheduled time + # and also, based on scheduled time + total = 0 + + in_scheduled_time = 0 + in_wait_window_time = 0 + + # the initial time joined in the initial 'waiting window' + initial_join_in_scheduled_time = nil + + # the amount of time spent in the initial 'waiting window' + initial_wait_time_in_scheduled_time = 0 + + last_wait_time_out = nil + + joined_on_time = false + waited_correctly = false + no_show = true + joined_late = false + joined_in_wait_window = false + ranges.each do |range| + time = range.end - range.begin + + total += time + + in_session_range = intersect(range, session_window) + in_join_window_range = intersect(range, initial_join_window) + in_wait_window_range = intersect(range, initial_wait_window) + + if in_session_range + in_scheduled_time += in_session_range.end - in_session_range.begin + no_show = false + end + + if in_join_window_range + if initial_join_in_scheduled_time.nil? + initial_join_in_scheduled_time = in_join_window_range.begin + end + joined_on_time = true + end + + if in_wait_window_range + in_wait_window_time += in_wait_window_range.end - in_wait_window_range.begin + last_wait_time_out = range.end + joined_in_wait_window = true + end + end + + if joined_in_wait_window && !joined_on_time + joined_late = true + end + + if last_wait_time_out && last_wait_time_out > wait_boundary_end + last_wait_time_out = wait_boundary_end + end + + initial_waiting_time_pct = nil + potential_waiting_time = nil + + # let's see if this person was hanging around for the bulk of this waiting window (to rule out someone coming/going very fast, trying to miss someone) + if last_wait_time_out && initial_join_in_scheduled_time + total_in_waiting_time = 0 + potential_waiting_range = Range.new(initial_join_in_scheduled_time, last_wait_time_out) + ranges.each do |range| + in_waiting = intersect(potential_waiting_range, range) + if in_waiting + total_in_waiting_time += in_waiting.end - in_waiting.begin + end + end + + potential_waiting_time = last_wait_time_out - initial_join_in_scheduled_time + initial_waiting_time_pct = total_in_waiting_time.to_f / potential_waiting_time.to_f + + # finally with all this stuff calculated, we can check: + # 1) did they wait a solid % of time between the time they joined, and left, during the initial 10 minute waiting window? + # 2) did they + if (initial_waiting_time_pct >= APP_CONFIG.wait_time_window_pct) && + (last_wait_time_out >= (wait_boundary_end - (APP_CONFIG.end_of_wait_window_forgiveness_minutes * 60))) + waited_correctly = true + end + end + + + # percentage computation of time spent during the session time + in_scheduled_percentage = in_scheduled_time.to_f / planned_duration_seconds.to_f + + # missed is an aggregrate concept shown in the UI often + # if you were a no show, or joined late, or didn't wait correctly, then you 'missed' + missed = no_show || joined_late || !waited_correctly + + joined_on_time = joined_on_time + { + total_time: total, + session_time: in_scheduled_time, + session_pct: in_scheduled_percentage, + joined_on_time: joined_on_time, + waited_correctly: waited_correctly, + no_show: no_show, + joined_late: joined_late, + missed: missed, + initial_join_in_scheduled_time: initial_join_in_scheduled_time, + last_wait_time_out: last_wait_time_out, + in_wait_window_time: in_wait_window_time, + initial_waiting_pct: initial_waiting_time_pct, + potential_waiting_time: potential_waiting_time + } + end + + # not OK for time objects. Here for docs more than anything + def self.intersect2(a, b) + min, max = a.first, a.exclude_end? ? a.max : a.last + other_min, other_max = b.first, b.exclude_end? ? b.max : b.last + + new_min = a === other_min ? other_min : b === min ? min : nil + new_max = a === other_max ? other_max : b === max ? max : nil + + new_min && new_max ? Range.new(new_min, new_max) : nil + end + + + def self.contained_by(a, test) + min, max = a.first, a.exclude_end? ? a.max : a.last + + test >= min && test <= max + end + + def self.intersect(a, b) + min, max = a.first, a.exclude_end? ? a.max : a.last + other_min, other_max = b.first, b.exclude_end? ? b.max : b.last + + new_min = contained_by(a, other_min) ? other_min : contained_by(b, min) ? min : nil + new_max = contained_by(a, other_max) ? other_max : contained_by(b, max) ? max : nil + + new_min && new_max ? Range.new(new_min, new_max) : nil + end + + def self.time_ranges(histories) + ranges = [] + histories.each do |history| + ranges << history.range + end + ranges + end + + def self.ranges_overlap?(a, b) + a.cover?(b.begin) || b.cover?(a.begin) + end + + def self.merge_ranges(a, b) + [a.begin, b.begin].min..[a.end, b.end].max + end + + def self.merge_overlapping_ranges(ranges) + ranges.sort_by(&:begin).inject([]) do |ranges, range| + if !ranges.empty? && ranges_overlap?(ranges.last, range) + ranges[0...-1] + [merge_ranges(ranges.last, range)] + else + ranges + [range] + end + end + end + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/lesson_session_monthly_price.rb b/ruby/lib/jam_ruby/models/lesson_session_monthly_price.rb new file mode 100644 index 000000000..b2eae100c --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_session_monthly_price.rb @@ -0,0 +1,40 @@ +module JamRuby + class LessonSessionMonthlyPrice + + # calculate the price for a given month + def self.price(lesson_booking, start_day) + + raise "lesson_booking is not monthly paid #{lesson_booking.admin_url}" if !lesson_booking.is_monthly_payment? + + data = lesson_booking.predicted_times_for_month(start_day.year, start_day.month) + + times = data[:times] + session = data[:session] + + true_start = start_day + if session + # if there is already a session for the month, that is the real star + true_start = session.scheduled_start.to_date + end + + # filter out anything before the start day + times.select! { |time| time.to_date >= true_start } + + result = nil + if times.length == 0 + result = 0 + elsif times.length == 1 + result = (lesson_booking.booked_price * 0.25).round(2) + elsif times.length == 2 + result = (lesson_booking.booked_price * 0.50).round(2) + elsif times.length == 3 + result = (lesson_booking.booked_price * 0.75).round(2) + else + result = lesson_booking.booked_price + end + + result + end + end +end + diff --git a/ruby/lib/jam_ruby/models/music_notation.rb b/ruby/lib/jam_ruby/models/music_notation.rb index fc2af0880..21406b384 100644 --- a/ruby/lib/jam_ruby/models/music_notation.rb +++ b/ruby/lib/jam_ruby/models/music_notation.rb @@ -4,6 +4,10 @@ module JamRuby NOTATION_FILE_DIR = "music_session_notations" + TYPE_NOTATION = 'notation' + TYPE_AUDIO = 'audio' + + ATTACHMENT_TYPES = [TYPE_NOTATION, TYPE_AUDIO] self.primary_key = 'id' attr_accessible :file_url, :size, :file_name @@ -16,10 +20,12 @@ module JamRuby before_destroy :delete_s3_files #validates :file_url, :presence => true + validates :attachment_type, :presence => true, inclusion: {in: ATTACHMENT_TYPES} validates :size, :presence => true - def self.create(session_id, file, current_user) + def self.create(session_id, type, file, current_user) music_notation = MusicNotation.new + music_notation.attachment_type = type music_notation.file_name = file.original_filename music_notation.music_session_id = session_id music_notation.user = current_user @@ -42,6 +48,9 @@ module JamRuby s3_manager.sign_url(self[:file_url], {:expires => expiration_time, :secure => true}) end + def is_notation? + self.attachment_type == TYPE_NOTATION + end private def self.construct_filename(notation) diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index feccafc42..d55ebf1ad 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -21,6 +21,8 @@ module JamRuby CREATE_TYPE_RSVP = 'rsvp' CREATE_TYPE_IMMEDIATE = 'immediately' CREATE_TYPE_QUICK_START = 'quick-start' + CREATE_TYPE_LESSON = 'lesson' + CREATE_TYPE_QUICK_PUBLIC = 'quick-public' attr_accessor :legal_terms, :language_description, :access_description, :scheduling_info_changed @@ -31,10 +33,10 @@ module JamRuby self.primary_key = 'id' belongs_to :creator,:class_name => 'JamRuby::User', :foreign_key => :user_id, :inverse_of => :music_session_histories - belongs_to :band, :class_name => 'JamRuby::Band', :foreign_key => :band_id, :inverse_of => :music_sessions - belongs_to :active_music_session, :class_name => 'JamRuby::ActiveMusicSession', foreign_key: :music_session_id + belongs_to :session_controller, :class_name => 'JamRuby::User', :foreign_key => :session_controller_id, :inverse_of => :controlled_sessions + belongs_to :lesson_session, :class_name => "JamRuby::LessonSession" has_many :music_session_user_histories, :class_name => "JamRuby::MusicSessionUserHistory", :foreign_key => "music_session_id", :dependent => :delete_all has_many :comments, :class_name => "JamRuby::MusicSessionComment", :foreign_key => "music_session_id" @@ -51,6 +53,7 @@ module JamRuby has_many :invited_fans, :through => :fan_invitations, :class_name => "JamRuby::User", :foreign_key => "receiver_id", :source => :receiver has_many :rsvp_slots, :class_name => "JamRuby::RsvpSlot", :foreign_key => "music_session_id", :dependent => :destroy has_many :music_notations, :class_name => "JamRuby::MusicNotation", :foreign_key => "music_session_id" + has_many :jam_track_session, :class_name => "JamRuby::JamTrackSession" validates :genre, :presence => true validates :description, :presence => true, :no_profanity => true @@ -116,6 +119,7 @@ module JamRuby new_session.open_rsvps = self.open_rsvps new_session.is_unstructured_rsvp = self.is_unstructured_rsvp new_session.legal_terms = true + new_session.session_controller = self.session_controller # copy rsvp_slots, rsvp_requests, and rsvp_requests_rsvp_slots RsvpSlot.where("music_session_id = '#{self.id}'").find_each do |slot| @@ -186,6 +190,10 @@ module JamRuby end end + def is_lesson? + !!lesson_session + end + def grouped_tracks tracks = [] self.music_session_user_histories.each do |msuh| @@ -255,6 +263,30 @@ module JamRuby end end + def set_session_controller(current_user, user) + + # only allow update of session controller by the creator or the currently marked user + + should_tick = false + + if current_user != creator && current_user != self.session_controller + return should_tick + end + + if active_music_session + if user + if active_music_session.users.exists?(user) + self.session_controller = user + should_tick = save + end + else + self.session_controller = nil + should_tick = save + end + end + should_tick + end + def self.index(current_user, user_id, band_id = nil, genre = nil) hide_private = false if current_user.id != user_id @@ -292,11 +324,29 @@ module JamRuby # let session be restarted for up to 2 hours after finishing session_finished = "(music_sessions.session_removed_at > NOW() - '2 hour'::INTERVAL)" - query = MusicSession.where("music_sessions.canceled = FALSE") + query = MusicSession.joins( + %Q{ + LEFT OUTER JOIN + invitations + ON + music_sessions.id = invitations.music_session_id AND invitations.receiver_id = '#{user.id}' + } + ) + query = query.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.user_id = '#{user.id}' OR invitations.id IS NOT NULL") + query = query.where("music_sessions.id in ( + select distinct(rs.music_session_id) + from rsvp_slots rs + where rs.id in ( + select rrrs.rsvp_slot_id + from rsvp_requests rr + inner join rsvp_requests_rsvp_slots rrrs on rr.id = rrrs.rsvp_request_id + where rr.user_id = '#{user.id}'AND rrrs.chosen = true + ) + ) OR invitations.id IS NOT NULL OR 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}'") + query = query.where("music_sessions.create_type IS NULL OR (music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}' AND music_sessions.create_type != '#{CREATE_TYPE_QUICK_PUBLIC}')") query = query.order("music_sessions.scheduled_start ASC") query @@ -308,7 +358,7 @@ module JamRuby filter_approved = only_approved ? 'AND rrrs.chosen = true' : '' MusicSession.where(%Q{music_sessions.canceled = FALSE AND - (music_sessions.create_type is NULL OR music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}') AND + (music_sessions.create_type is NULL OR (music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}' AND music_sessions.create_type != '#{CREATE_TYPE_QUICK_PUBLIC}')) AND (music_sessions.scheduled_start is NULL OR music_sessions.scheduled_start > NOW() - '4 hour'::INTERVAL) AND music_sessions.id in ( select distinct(rs.music_session_id) @@ -343,9 +393,15 @@ module JamRuby ms.legal_terms = true ms.open_rsvps = options[:open_rsvps] if options[:open_rsvps] ms.creator = user + ms.session_controller = user ms.create_type = options[:create_type] ms.is_unstructured_rsvp = options[:isUnstructuredRsvp] if options[:isUnstructuredRsvp] ms.scheduled_start = parse_scheduled_start(options[:start], options[:timezone]) if options[:start] && options[:timezone] + ms.scheduled_start = options[:scheduled_start] if options[:scheduled_start] + if options[:lesson_session] + ms.lesson_session = options[:lesson_session] + end + ms.save @@ -385,6 +441,10 @@ module JamRuby case ms.create_type when CREATE_TYPE_RSVP, CREATE_TYPE_SCHEDULE_FUTURE Notification.send_scheduled_session_invitation(ms, receiver) + when CREATE_TYPE_LESSON + if ms.lesson_session.is_active? + Notification.send_jamclass_invitation_teacher(ms, receiver) + end else Notification.send_session_invitation(receiver, user, ms.id) end @@ -719,7 +779,7 @@ module JamRuby query = query.offset(offset) query = query.limit(limit) - query = query.where("music_sessions.create_type IS NULL OR (music_sessions.create_type != ? AND music_sessions.create_type != ?)", MusicSession::CREATE_TYPE_QUICK_START, MusicSession::CREATE_TYPE_IMMEDIATE) + query = query.where("music_sessions.create_type IS NULL OR (music_sessions.create_type != ? AND music_sessions.create_type != ? AND music_sessions.create_type != ?)", MusicSession::CREATE_TYPE_QUICK_START, MusicSession::CREATE_TYPE_IMMEDIATE, MusicSession::CREATE_TYPE_QUICK_PUBLIC) query = query.where("music_sessions.genre_id = ?", genre) unless genre.blank? query = query.where('music_sessions.language = ?', lang) unless lang.blank? query = query.where("(description_tsv @@ to_tsquery('jamenglish', ?))", ActiveRecord::Base.connection.quote(keyword) + ':*') unless keyword.blank? @@ -835,7 +895,7 @@ SQL result end - def scheduled_start_date + def scheduled_start_date if self.scheduled_start_time.blank? "" else @@ -890,20 +950,16 @@ SQL end duration end - # should create a timestamp like: - # - # with_timezone = TRUE - # Tuesday, April 29, 8:00-9:00 PM TIMEZONE (where TIMEZONE is the TIMEZONE defined in the MusicSession when it was created) - # - # with_timezone = FALSE - # Thursday, July 10 - 10:00pm - # this should be in a helper - def pretty_scheduled_start(with_timezone) + + def pretty_scheduled_start(with_timezone = true, shorter = false) if scheduled_start && scheduled_duration start_time = scheduled_start timezone_display = 'UTC' + utc_offset_display = '00:00' tz_identifier, tz_display = MusicSession.split_timezone(timezone) + short_tz = 'GMT' + begin tz = TZInfo::Timezone.get(tz_identifier) rescue Exception => e @@ -913,17 +969,32 @@ SQL if tz begin start_time = tz.utc_to_local(scheduled_start.utc) - timezone_display = tz_display + timezone_display = tz.pretty_name + utc_offset_hours = tz.current_period.utc_total_offset / (60*60) + hour = sprintf '%02d', utc_offset_hours.abs + minutes = sprintf '%02d', ((tz.current_period.utc_total_offset.abs % 3600) / 3600) * 60 + utc_offset_display = "#{utc_offset_hours < 0 ? '-' : ' '}#{hour}:#{minutes}" + short_tz = start_time.strftime("%Z") + if short_tz == 'UTC' + short_tz = 'GMT' + end + rescue Exception => e @@log.error("unable to convert #{scheduled_start} to #{tz}, e=#{e}") puts "unable to convert #{e}" end end + duration = safe_scheduled_duration end_time = start_time + duration if with_timezone - "#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} #{timezone_display}" + if shorter + #"#{start_time.strftime("%a, %b %e %Y")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} (#{short_tz}#{utc_offset_display})" + "#{start_time.strftime("%a, %b %e %Y")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} (#{timezone_display})" + else + "#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} #{timezone_display}" + end else "#{start_time.strftime("%A, %B %e")} - #{start_time.strftime("%l:%M%P").strip}" end @@ -937,7 +1008,7 @@ SQL def self.purgeable_sessions sessions = [] sql =< -1..1, :allow_nil => true - belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id", :inverse_of => :music_session_user_histories - belongs_to :music_session, :class_name => "MusicSession", :foreign_key => "music_session_id" + belongs_to :user, :class_name => "JamRuby::User", :foreign_key => :user_id, :inverse_of => :music_session_user_histories + belongs_to :music_session, :class_name => "MusicSession", :foreign_key => :music_session_id def self.latest_history(client_id) self.where(:client_id => client_id) @@ -21,6 +21,16 @@ module JamRuby .first end + def name + user.name + end + + def range + # to_time to not use ActiveSupport::TimeWithZone + session_removed_at_time = session_removed_at.to_time if session_removed_at + Range.new(created_at.to_time, session_removed_at_time || Time.now.to_time) + end + def music_session @msh ||= JamRuby::MusicSession.find_by_music_session_id(self.music_session_id) end @@ -30,9 +40,9 @@ module JamRuby end def self.save(music_session_id, user_id, client_id, tracks) - return true if 0 < self.where(:music_session_id => music_session_id, - :user_id => user_id, - :client_id => client_id).count + return true if 0 < self.where(:music_session_id => music_session_id, + :user_id => user_id, + :client_id => client_id).where('session_removed_at is NULL').count session_user_history = MusicSessionUserHistory.new session_user_history.music_session_id = music_session_id session_user_history.user_id = user_id @@ -50,10 +60,12 @@ module JamRuby (end_time - self.created_at) / 60.0 end - def self.join_music_session(user_id, session_id) + def self.join_music_session(user_id, session_id, client_id) hist = self .where(:user_id => user_id) .where(:music_session_id => session_id) + .where(:client_id => client_id) + .where('session_removed_at IS NULL') .limit(1) .first hist.start_history if hist diff --git a/ruby/lib/jam_ruby/models/news.rb b/ruby/lib/jam_ruby/models/news.rb new file mode 100644 index 000000000..0d126ad38 --- /dev/null +++ b/ruby/lib/jam_ruby/models/news.rb @@ -0,0 +1,14 @@ + +module JamRuby + class News < ActiveRecord::Base + + self.table_name = 'news' + + attr_accessible :title, :body, :position, as: :admin + default_scope { order('position') } + validates :title, presence: true + validates :body, presence: true + validates :position, presence: true, numericality: {only_integer: true}, uniqueness: true + + end +end diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index e635014e8..0f46e36d6 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -12,8 +12,10 @@ module JamRuby belongs_to :source_user, :class_name => "JamRuby::User", :foreign_key => "source_user_id" belongs_to :band, :class_name => "JamRuby::Band", :foreign_key => "band_id" belongs_to :music_session, :class_name => "JamRuby::MusicSession", :foreign_key => "music_session_id" + belongs_to :lesson_session, :class_name => "JamRuby::LessonSession", :foreign_key => "lesson_session_id" belongs_to :recording, :class_name => "JamRuby::Recording", :foreign_key => "recording_id" belongs_to :jam_track_right, :class_name => "JamRuby::JamTrackRight", :foreign_key => "jam_track_right_id" + belongs_to :jam_track_mixdown_package, :class_name => "JamRuby::JamTrackMixdownPackage", :foreign_key => "jam_track_mixdown_package_id" validates :target_user, :presence => true validates :message, length: {minimum: 1, maximum: 400}, no_profanity: true, if: :text_message? @@ -61,7 +63,7 @@ module JamRuby end end - self.class.format_msg(self.description, {:user => source_user, :band => band, :session => session}) + self.class.format_msg(self.description, {:user => source_user, target: target_user, :band => band, :session => session, purpose: purpose, student_directed: student_directed, msg: message}) end # TODO: MAKE ALL METHODS BELOW ASYNC SO THE CLIENT DOESN'T BLOCK ON NOTIFICATION LOGIC @@ -131,6 +133,9 @@ module JamRuby user = options[:user] band = options[:band] session = options[:session] + purpose = options[:purpose] + student_directed = options[:student_directed] + msg = options[:msg] name, band_name = "" unless user.nil? @@ -248,8 +253,39 @@ module JamRuby when NotificationTypes::BAND_INVITATION_ACCEPTED return "#{name} has accepted your band invitation to join #{band_name}." + when NotificationTypes::LESSON_MESSAGE + notification_msg = 'Lesson Changed' + + if purpose == 'requested' + notification_msg = 'You have received a lesson request' + elsif purpose == 'accept' + notification_msg = 'Your lesson request is confirmed!' + elsif purpose == 'declined' + notification_msg = "We're sorry your lesson request has been declined." + elsif purpose == 'canceled' + notification_msg = "Your lesson request has been canceled." + elsif purpose == 'counter' + if student_directed + notification_msg = "Instructor has proposed a different time for your lesson." + else + notification_msg = "Student has proposed a different time for your lesson." + end + elsif purpose == 'reschedule' + 'A lesson reschedule has been requested' + elsif purpose == 'chat' + notification_msg = "Lesson Message: #{msg}" + end + return notification_msg + + when NotificationTypes::SCHEDULED_JAMCLASS_INVITATION + if student_directed + "You have been scheduled to take a JamClass with #{user.name}." + else + "You have been scheduled to teach a JamClass to #{user.name}" + end + else - return "" + return description end end @@ -371,6 +407,51 @@ module JamRuby end end + def send_lesson_message(purpose, lesson_session, student_directed, msg = nil) + + notification = Notification.new + notification.description = NotificationTypes::LESSON_MESSAGE + notification.student_directed = student_directed + + if !student_directed + notification.source_user_id = lesson_session.student.id + notification.target_user_id = lesson_session.teacher.id + else + notification.source_user_id = lesson_session.teacher.id + notification.target_user_id = lesson_session.student.id + end + + notification.purpose = purpose + notification.session_id = lesson_session.music_session.id + notification.lesson_session_id = lesson_session.id + + notification_msg = format_msg(NotificationTypes::LESSON_MESSAGE, {purpose: purpose, msg: msg}) + + if purpose == 'chat' + notification.message = msg + end + + notification.save + + # receiver_id, sender_photo_url, sender_name, sender_id, msg, clipped_msg, notification_id, created_at + message = @@message_factory.lesson_message( + notification.target_user.id, + notification.source_user.resolved_photo_url, + notification.source_user.name, + notification.source_user.id, + notification_msg, + notification.id, + notification.session_id, + notification.created_date, + notification.student_directed, + notification.purpose, + notification.lesson_session_id + ) + + @@mq_router.publish_to_user(notification.target_user.id, message) + + end + def send_new_band_follower(follower, band) band.band_musicians.each.each do |bm| @@ -528,15 +609,28 @@ module JamRuby notification_msg = format_msg(NotificationTypes::SESSION_JOIN, {:user => user}) - msg = @@message_factory.session_join( + sessionMsg = @@message_factory.session_join( active_music_session.id, user.photo_url, user.id, notification_msg, - active_music_session.track_changes_counter + active_music_session.track_changes_counter, + connection.client_id, + MessageFactory::CLIENT_TARGET ) - @@mq_router.server_publish_to_session(active_music_session, msg, sender = {:client_id => connection.client_id}) + userMsg = @@message_factory.session_join( + active_music_session.id, + user.photo_url, + user.id, + notification_msg, + active_music_session.track_changes_counter, + connection.client_id, + MessageFactory::USER_TARGET_PREFIX + user.id + ) + + @@mq_router.server_publish_to_session(active_music_session, sessionMsg, sender = {:client_id => connection.client_id}) + @@mq_router.publish_to_user(user.id, userMsg, sender = {:client_id => connection.client_id}) end def send_session_depart(active_music_session, client_id, user, recordingId) @@ -548,10 +642,26 @@ module JamRuby user.photo_url, notification_msg, recordingId, - active_music_session.track_changes_counter + active_music_session.track_changes_counter, + client_id, + user.id, + MessageFactory::CLIENT_TARGET + ) + + userMsg = @@message_factory.session_depart( + active_music_session.id, + user.photo_url, + notification_msg, + recordingId, + active_music_session.track_changes_counter, + client_id, + user.id, + MessageFactory::USER_TARGET_PREFIX + user.id ) @@mq_router.server_publish_to_session(active_music_session, msg, sender = {:client_id => client_id}) + @@mq_router.publish_to_user(user.id, userMsg, sender = {:client_id => client_id}) + end def send_tracks_changed(active_music_session) @@ -614,44 +724,127 @@ module JamRuby end end - def send_scheduled_session_invitation(music_session, user) - + def send_jamclass_invitation_teacher(music_session, user) return if music_session.nil? || user.nil? - target_user = user - source_user = music_session.creator + teacher = target_user = user + student = source_user = music_session.creator + + notification_msg = format_msg(NotificationTypes::SCHEDULED_JAMCLASS_INVITATION, {user: student, student_directed: false}) notification = Notification.new - notification.description = NotificationTypes::SCHEDULED_SESSION_INVITATION + notification.description = NotificationTypes::SCHEDULED_JAMCLASS_INVITATION notification.source_user_id = source_user.id notification.target_user_id = target_user.id notification.session_id = music_session.id + notification.lesson_session_id = music_session.lesson_session.id + notification.student_directed = false + #notification.message = notification_msg notification.save - notification_msg = format_msg(notification.description, {:user => source_user, :session => music_session}) - - if target_user.online? - msg = @@message_factory.scheduled_session_invitation( - target_user.id, - music_session.id, - source_user.photo_url, - notification_msg, - music_session.name, - music_session.pretty_scheduled_start(false), - notification.id, - notification.created_date + if target_user.online + msg = @@message_factory.scheduled_jamclass_invitation( + target_user.id, + music_session.id, + source_user.photo_url, + notification_msg, + music_session.name, + music_session.pretty_scheduled_start(false), + notification.id, + notification.created_date, + notification.lesson_session.id ) @@mq_router.publish_to_user(target_user.id, msg) end begin - UserMailer.scheduled_session_invitation(target_user, notification_msg, music_session).deliver_now + #UserMailer.teacher_scheduled_jamclass_invitation(music_session.lesson_session.teacher, notification_msg, music_session).deliver_now rescue => e - @@log.error("Unable to send SCHEDULED_SESSION_INVITATION email to user #{target_user.email} #{e}") + @@log.error("Unable to send SCHEDULED_JAMCLASS_INVITATION email to user #{music_session.lesson_session.teacher.email} #{e}") end end + def send_student_jamclass_invitation(music_session, user) + return if music_session.nil? || user.nil? + + student = target_user = user + teacher = source_user = music_session.lesson_session.teacher + notification_msg = format_msg(NotificationTypes::SCHEDULED_JAMCLASS_INVITATION, {user: teacher, student_directed: true}) + + notification = Notification.new + notification.description = NotificationTypes::SCHEDULED_JAMCLASS_INVITATION + notification.source_user_id = source_user.id + notification.target_user_id = target_user.id + notification.session_id = music_session.id + notification.lesson_session_id = music_session.lesson_session.id + notification.student_directed = true + #notification.message = notification_msg + notification.save + + if target_user.online + msg = @@message_factory.scheduled_jamclass_invitation( + target_user.id, + music_session.id, + source_user.photo_url, + notification_msg, + music_session.name, + music_session.pretty_scheduled_start(false), + notification.id, + notification.created_date, + notification.lesson_session_id + ) + + @@mq_router.publish_to_user(target_user.id, msg) + end + + begin + #UserMailer.student_scheduled_jamclass_invitation(student, notification_msg, music_session).deliver_now + rescue => e + @@log.error("Unable to send SCHEDULED_JAMCLASS_INVITATION email to user #{student.email} #{e}") + end + + end + + + def send_scheduled_session_invitation(music_session, user) + + return if music_session.nil? || user.nil? + + target_user = user + source_user = music_session.creator + + notification = Notification.new + notification.description = NotificationTypes::SCHEDULED_SESSION_INVITATION + notification.source_user_id = source_user.id + notification.target_user_id = target_user.id + notification.session_id = music_session.id + notification.save + + notification_msg = format_msg(notification.description, {:user => source_user, :session => music_session}) + + if target_user.online + msg = @@message_factory.scheduled_session_invitation( + target_user.id, + music_session.id, + source_user.photo_url, + notification_msg, + music_session.name, + music_session.pretty_scheduled_start(false), + notification.id, + notification.created_date + ) + + @@mq_router.publish_to_user(target_user.id, msg) + end + + begin + UserMailer.scheduled_session_invitation(target_user, notification_msg, music_session).deliver_now + rescue => e + @@log.error("Unable to send SCHEDULED_SESSION_INVITATION email to user #{target_user.email} #{e}") + end + end + def send_scheduled_session_rsvp(music_session, user, instruments) return if music_session.nil? || user.nil? @@ -905,7 +1098,7 @@ module JamRuby # start in less than 24 hours, and haven't been # notified for a particular interval yet: def send_session_reminders - MusicSession.where("scheduled_start > NOW() AND scheduled_start <= (NOW()+INTERVAL '1 DAYS')").each do |candidate_session| + MusicSession.where("scheduled_start > NOW() AND scheduled_start <= (NOW()+INTERVAL '1 DAYS') AND lesson_session_id IS NULL").each do |candidate_session| tm = candidate_session.scheduled_start if (tm>(12.hours.from_now) && !notified?(candidate_session, NotificationTypes::SCHEDULED_SESSION_REMINDER_DAY)) # Send 24 hour reminders: @@ -1256,7 +1449,7 @@ module JamRuby def send_jam_track_sign_complete(jam_track_right) notification = Notification.new - notification.jam_track_right_id = jam_track_right.id + notification.jam_track_mixdown_package = jam_track_right.id notification.description = NotificationTypes::JAM_TRACK_SIGN_COMPLETE notification.target_user_id = jam_track_right.user_id notification.save! @@ -1266,6 +1459,30 @@ module JamRuby #@@mq_router.publish_to_all_clients(msg) end + def send_mixdown_sign_failed(jam_track_mixdown_package) + + notification = Notification.new + notification.jam_track_mixdown_package_id = jam_track_mixdown_package.id + notification.description = NotificationTypes::MIXDOWN_SIGN_FAILED + notification.target_user_id = jam_track_mixdown_package.jam_track_mixdown.user_id + notification.save! + + msg = @@message_factory.mixdown_sign_failed(jam_track_mixdown_package.jam_track_mixdown.user_id, jam_track_mixdown_package.id) + @@mq_router.publish_to_user(jam_track_mixdown_package.jam_track_mixdown.user_id, msg) + end + + def send_mixdown_sign_complete(jam_track_mixdown_package) + + notification = Notification.new + notification.jam_track_mixdown_package_id = jam_track_mixdown_package.id + notification.description = NotificationTypes::MIXDOWN_SIGN_COMPLETE + notification.target_user_id = jam_track_mixdown_package.jam_track_mixdown.user_id + notification.save! + + msg = @@message_factory.mixdown_sign_complete(jam_track_mixdown_package.jam_track_mixdown.user_id, jam_track_mixdown_package.id) + @@mq_router.publish_to_user(jam_track_mixdown_package.jam_track_mixdown.user_id, msg) + end + def send_client_update(product, version, uri, size) msg = @@message_factory.client_update( product, version, uri, size) diff --git a/ruby/lib/jam_ruby/models/payment_history.rb b/ruby/lib/jam_ruby/models/payment_history.rb index 4862e4dd1..ab6013d62 100644 --- a/ruby/lib/jam_ruby/models/payment_history.rb +++ b/ruby/lib/jam_ruby/models/payment_history.rb @@ -5,6 +5,7 @@ module JamRuby belongs_to :sale belongs_to :recurly_transaction_web_hook + belongs_to :charge def self.index(user, params = {}) @@ -14,7 +15,7 @@ module JamRuby limit = limit.to_i query = PaymentHistory.limit(limit) - .includes(sale: [:sale_line_items], recurly_transaction_web_hook:[]) + .includes(sale: [:sale_line_items], recurly_transaction_web_hook:[], charge:[]) .where(user_id: user.id) .where("transaction_type = 'sale' OR transaction_type = 'refund' OR transaction_type = 'void'") .order('created_at DESC') diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb index e812402f4..28061f341 100644 --- a/ruby/lib/jam_ruby/models/recording.rb +++ b/ruby/lib/jam_ruby/models/recording.rb @@ -201,11 +201,19 @@ module JamRuby def has_access?(user) - !user.nil? && (users.exists?(user.id) || plays.where("player_id=?", user).count != 0) + return false if user.nil? + + users.exists?(user.id) || attached_with_lesson(user) #|| plays.where("player_id=?", user).count != 0 + end + + def attached_with_lesson(user) + + ChatMessage.joins(:claimed_recording => [:recording]).where('recordings.id = ?', self.id).where('chat_messages.user_id = ?', user.id).count > 0 || + ChatMessage.joins(:claimed_recording => [:recording]).where('recordings.id = ?', self.id).where('chat_messages.target_user_id = ?', user.id).count > 0 end # Start recording a session. - def self.start(music_session, owner) + def self.start(music_session, owner, record_video: false) recording = nil # Use a transaction and lock to avoid races. music_session.with_lock do @@ -213,9 +221,10 @@ module JamRuby recording.music_session = music_session recording.owner = owner recording.band = music_session.band + recording.video = record_video if recording.save - GoogleAnalyticsEvent.report_band_recording(recording.band) + #GoogleAnalyticsEvent.report_band_recording(recording.band) # make quick mixes *before* the audio/video tracks, because this will give them precedence in list_uploads music_session.users.uniq.each do |user| @@ -700,6 +709,10 @@ module JamRuby self.save(:validate => false) end + def add_video_data(data) + Recording.where(id: self.id).update_all(external_video_id: data[:video_id]) + end + def add_timeline(timeline) global = timeline["global"] raise JamArgumentError, "global must be specified" unless global 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 88baa78ae..ca1bd4cb7 100644 --- a/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb +++ b/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb @@ -92,53 +92,15 @@ module JamRuby transaction.save! # 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' 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! + if sale + AdminMailer.recurly_alerts(transaction.user, { + subject: "ACTION REQUIRED: #{transaction.user.email} has refund on invoice", + body: "You will have to manually revoke any JamTrackRights in our database for the appropriate JamTracks" + }).deliver_now - 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_now - 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_now - 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_now - 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_now - end else AdminMailer.recurly_alerts(transaction.user, { subject: "ACTION REQUIRED: #{transaction.user.email} has refund with no correlator to sales", diff --git a/ruby/lib/jam_ruby/models/review.rb b/ruby/lib/jam_ruby/models/review.rb new file mode 100644 index 000000000..7e7e8bd6d --- /dev/null +++ b/ruby/lib/jam_ruby/models/review.rb @@ -0,0 +1,142 @@ +module JamRuby + class Review < ActiveRecord::Base + include HtmlSanitize + html_sanitize strict: [:description] + + attr_accessible :target, :rating, :description, :user, :user_id, :target_id, :target_type + belongs_to :target, polymorphic: true + belongs_to :user, foreign_key: 'user_id', class_name: "JamRuby::User" + belongs_to :deleted_by_user, foreign_key: 'deleted_by_user_id', class_name: "JamRuby::User" + + scope :available, -> { where("deleted_at iS NULL") } + + validates :description, length: {maximum: 16000}, no_profanity: true, :allow_blank => true + validates :rating, presence: true, numericality: {only_integer: true, minimum: 1, maximum: 5} + + validates :target, presence: true + validates :user_id, presence: true + validates :target_id, uniqueness: {scope: :user_id, message: "There is already a review for this User and Target."} + validate :requires_lesson + + after_save :reduce + + def requires_lesson + if target_type == 'JamRuby::User' + + # you are rating a student + lesson = LessonSession.joins(:music_session).where('music_sessions.user_id = ?', target.id).where(teacher_id: user.id).first + if lesson.nil? + errors.add(:target, "You must have at least scheduled or been in a lesson with this student") + end + + elsif target_type == "JamRuby::Teacher" + + # you are rating a teacher + lesson = LessonSession.joins(:music_session).where('music_sessions.user_id = ?', user.id).where(teacher_id: target.user.id).first + if lesson.nil? + errors.add(:target, "You must have at least scheduled or been in a lesson with this teacher") + end + end + end + + def self.create_or_update(params) + review = Review.where(user_id: params[:user].id).where(target_id: params[:target].id).where(target_type: params[:target].class.to_s).first + + if review + review.description = params[:description] + review.rating = params[:rating] + review.save + else + review = Review.create(params) + end + + review + end + def self.create(params) + review = Review.new + review.target = params[:target] + review.user = params[:user] + review.rating = params[:rating] + review.description = params[:description] + review.target_type = params[:target].class.to_s + review.save + review + end + + def self.index(options={}) + if options.key?(:include_deleted) + arel = Review.all + else + arel = Review.available + end + + if options.key?(:target_id) + arel = arel.where("target_id=?", options[:target_id]) + end + + if options.key?(:user_id) + arel = arel.where("user_id=?", options[:user_id]) + end + + arel + end + + # Create review_summary records by grouping reviews + def self.reduce_all + ReviewSummary.transaction do + ReviewSummary.destroy_all + Review.select("target_id, target_type AS target_type, AVG(rating) as avg_rating, count(*) as review_count, SUM(CASE WHEN rating>=3.0 THEN 1 ELSE 0 END) AS pos_count") + .where("deleted_at IS NULL") + .group("target_type, target_id") + .each do |r| + wilson_score = ci_lower_bound(r.pos_count, r.review_count) + ReviewSummary.create!( + target_id: r.target_id, + target_type: r.target_type, + avg_rating: r.avg_rating, + wilson_score: wilson_score, + review_count: r.review_count + ) + end + end + end + + # http://www.evanmiller.org/how-not-to-sort-by-average-rating.html + def self.ci_lower_bound(pos, n, confidence=0.95) + pos=pos.to_f + n=n.to_f + return 0 if n == 0 + z = 1.96 # Statistics2.pnormaldist(1-(1-confidence)/2) + phat = 1.0*pos/n + (phat + z*z/(2*n) - z * Math.sqrt((phat*(1-phat)+z*z/(4*n))/n))/(1+z*z/n) + end + + def reduce + ReviewSummary.transaction do + ReviewSummary.where(target_type: target_type, target_id: target_id).delete_all + + Review.select("target_id, target_type AS target_type, AVG(rating) as avg_rating, count(*) as review_count, SUM(CASE WHEN rating>=3.0 THEN 1 ELSE 0 END) AS pos_count") + .where("deleted_at IS NULL") + .where(target_type: target_type, target_id: target_id) + .group("target_type, target_id") + .each do |r| + wilson_score = Review.ci_lower_bound(r.pos_count, r.review_count) + + summary = ReviewSummary.create( + target_id: r.target_id, + target_type: r.target_type, + avg_rating: r.avg_rating, + wilson_score: wilson_score, + review_count: r.review_count + ) + if summary.errors.any? + puts "review summary unable to be created #{summary.errors.inspect}" + raise "review summary unable to be created #{summary.errors.inspect}" + end + end + end + + return true + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/review_summary.rb b/ruby/lib/jam_ruby/models/review_summary.rb new file mode 100644 index 000000000..b0aef4711 --- /dev/null +++ b/ruby/lib/jam_ruby/models/review_summary.rb @@ -0,0 +1,43 @@ +module JamRuby + class ReviewSummary < ActiveRecord::Base + attr_accessible :target, :target_id, :target_type, :avg_rating, :wilson_score, :review_count + belongs_to :target, polymorphic: true + + validates :avg_rating, presence:true, numericality: true + validates :review_count, presence:true, numericality: {only_integer: true} + validates :wilson_score, presence:true, numericality: {greater_than_or_equal_to:0, less_than_or_equal_to:1} + validates :target_id, presence:true, uniqueness:true + + class << self + + # Query review_summaries using target type, id, and minimum review count + # * target_type: Only return review summaries for given target type + # * target_id: Only return review summary for given target type + # * minimum_reviews: Only return review summary made up of at least this many reviews + # * arel: start with pre-queried reviews (arel object) + # sorts by wilson score + def index(options={}) + options ||= {} + if (options.key?(:arel)) + arel = options[:arel].order("wilson_score DESC") + else + arel = ReviewSummary.order("wilson_score DESC") + end + + if (options.key?(:target_type)) + arel = arel.where("target_type=?", options[:target_type]) + end + + if (options.key?(:target_id)) + arel = arel.where("target_id=?", options[:target_id]) + end + + if (options.key?(:minimum_reviews)) + arel = arel.where("review_count>=?", options[:minimum_reviews]) + end + + arel + end + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb index d8be57134..0e96114fc 100644 --- a/ruby/lib/jam_ruby/models/sale.rb +++ b/ruby/lib/jam_ruby/models/sale.rb @@ -4,6 +4,10 @@ module JamRuby class Sale < ActiveRecord::Base JAMTRACK_SALE = 'jamtrack' + LESSON_SALE = 'lesson' + + SOURCE_RECURLY = 'recurly' + SOURCE_IOS = 'ios' belongs_to :user, class_name: 'JamRuby::User' has_many :sale_line_items, class_name: 'JamRuby::SaleLineItem' @@ -32,12 +36,12 @@ module JamRuby # 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} + 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 } + {query: query, next_page: next_page} end end @@ -69,27 +73,90 @@ module JamRuby } end - def self.preview_invoice(current_user, shopping_carts) + # The expectation is that this code would throw an exception (breaking the transaction that encompasses it), + # if it can't validate the receipt, or communicate with Apple at all, etc + # + # So, if this raises exceptions, you can handle them in the stubbed out begin/rescue in ApiJamTracksController#ios_order_placed + def self.validateIOSReceipt(receipt, price_data, user, sale) + # these are all 'in cents' (as painfully named to be very clear), and all expected to be integers + price = price_data['product_price'].to_f * 100.0 - line_items = {jam_tracks: []} - shopping_carts_jam_tracks = [] - shopping_carts_subscriptions = [] - shopping_carts.each do |shopping_cart| + price_info = { + subtotal_in_cents: price, + total_in_cents: price, + tax_in_cents: nil, + currency: price_data['product_currency'] + } + response = IosReceiptValidator.post('/verifyReceipt', + body: { 'receipt-data' => receipt }.to_json, + headers: { 'Content-Type' => 'application/json' }) + json_resp = JSON.parse(response.body) - if shopping_cart.is_jam_track? - shopping_carts_jam_tracks << shopping_cart + # https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1 + + if 0 != json_resp['status'] + err_msgs = { + 21000 => 'The App Store could not read the JSON object you provided.', + 21002 => 'The data in the receipt-data property was malformed or missing.', + 21003 => 'The receipt could not be authenticated.', + 21005 => 'The receipt server is not currently available.', + 21007 => 'This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.', + 21008 => 'This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead.' + } + raise err_msgs[json_resp['status']] + else + receiptJson = SaleReceiptIOS.new + receiptJson.user = user + receiptJson.sale = sale + receiptJson.data_blob = json_resp + receiptJson.save! + end + price_info + end + + def self.ios_purchase(current_user, jam_track, receipt, price_data) + jam_track_right = nil + + # everything needs to go into a transaction! If anything goes wrong, we need to raise an exception to break it + Sale.transaction do + + using_free_credit = current_user.redeem_free_credit + + sale = create_jam_track_sale(current_user, SOURCE_IOS) + + if sale.valid? + + if using_free_credit + SaleLineItem.create_from_jam_track(current_user, sale, jam_track, using_free_credit) + sale.recurly_subtotal_in_cents = 0 + sale.recurly_tax_in_cents = 0 + sale.recurly_total_in_cents = 0 + sale.recurly_currency = 'USD' + sale.save! + else + + price_info = validateIOSReceipt(receipt, price_data, current_user, sale) + + SaleLineItem.create_from_jam_track(current_user, sale, jam_track, using_free_credit) + sale.recurly_subtotal_in_cents = price_info[:subtotal_in_cents] + sale.recurly_tax_in_cents = price_info[:tax_in_cents] + sale.recurly_total_in_cents = price_info[:total_in_cents] + sale.recurly_currency = price_info[:currency] + sale.save! + end else - # XXX: this may have to be revisited when we actually have something other than JamTracks for puchase - shopping_carts_subscriptions << shopping_cart + # should not get out of testing. This would be very rare (i.e., from a big regression). Sale is always valid at this point. + raise "invalid sale object" + end + + # if we make it this far, all is well! + 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 = using_free_credit + jam_track_right.version = jam_track.version 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 + jam_track_right end # place_order will create one or more sales based on the contents of shopping_carts for the current user @@ -99,19 +166,14 @@ module JamRuby 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 + + if Sale.is_mixed(shopping_carts) + # the controller checks this too; this is just an extra-level of sanity checking + return sales end - jam_track_sale = order_jam_tracks(current_user, shopping_carts_jam_tracks) + jam_track_sale = order_jam_tracks(current_user, shopping_carts) sales << jam_track_sale if jam_track_sale # TODO: process shopping_carts_subscriptions @@ -119,32 +181,180 @@ module JamRuby 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 + def self.is_only_freebie(shopping_carts) + free = true + shopping_carts.each do |cart| + free = cart.product_info[:free] - # 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 + if !free + break + end + end + free end - def self.is_only_freebie(shopping_carts_jam_tracks) - shopping_carts_jam_tracks.length == 1 && shopping_carts_jam_tracks[0].product_info[:free] + # we don't allow mixed shopping carts :/ + def self.is_mixed(shopping_carts) + free = false + non_free = false + shopping_carts.each do |cart| + if cart.product_info[:free] + free = true + else + non_free = true + end + end + free && non_free end + def self.purchase_test_drive(current_user, lesson_package_type, booking = nil) + self.purchase_lesson(current_user, booking, lesson_package_type) + end + + def self.purchase_normal(current_user, booking) + self.purchase_lesson(current_user, booking, LessonPackageType.single, booking.lesson_sessions[0]) + end + + # this is easy to make generic, but right now, it just purchases lessons + def self.purchase_lesson(current_user, lesson_booking, lesson_package_type, lesson_session = nil, lesson_package_purchase = nil, force = false) + stripe_charge = nil + sale = nil + purchase = nil + # everything needs to go into a transaction! If anything goes wrong, we need to raise an exception to break it + Sale.transaction(:requires_new => true) do + + sale = create_lesson_sale(current_user) + + if sale.valid? + + if lesson_booking + lesson_booking.current_lesson = lesson_session + lesson_booking.current_purchase = lesson_package_purchase + end + + sale_line_item = SaleLineItem.create_from_lesson_package(current_user, sale, lesson_package_type, lesson_booking) + + price_info = charge_stripe_for_lesson(current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session, lesson_package_purchase, force) + + if price_info[:purchase] && price_info[:purchase].errors.any? + purchase = price_info[:purchase] + raise ActiveRecord::Rollback + end + + if !sale_line_item.valid? + raise "invalid sale_line_item object for user #{current_user.email} and lesson_booking #{lesson_booking.id}" + end + # sale.source = 'stripe' + sale.recurly_subtotal_in_cents = price_info[:subtotal_in_cents] + sale.recurly_tax_in_cents = price_info[:tax_in_cents] + sale.recurly_total_in_cents = price_info[:total_in_cents] + sale.recurly_currency = price_info[:currency] + sale.stripe_charge_id = price_info[:charge_id] + sale.save + stripe_charge = price_info[:charge] + purchase = price_info[:purchase] + else + # should not get out of testing. This would be very rare (i.e., from a big regression). Sale is always valid at this point. + puts "invalid sale object" + raise "invalid sale object" + end + + end + + {sale: sale, stripe_charge: stripe_charge, purchase: purchase} + end + + def self.charge_stripe_for_lesson(current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session = nil, lesson_package_purchase = nil, force = false) + if lesson_package_purchase + target = lesson_package_purchase + elsif lesson_session + target = lesson_session + else + target = lesson_package_type + end + + current_user.sync_stripe_customer + + purchase = lesson_package_purchase + purchase = LessonPackagePurchase.create(current_user, lesson_booking, lesson_package_type) if purchase.nil? + + if purchase.errors.any? + puts "purchase errors #{purchase.errors.inspect}" + price_info = {} + price_info[:purchase] = purchase + return price_info + end + + if lesson_session + lesson_session.lesson_package_purchase_id = purchase.id + lesson_session.save! + end + + subtotal_in_cents = purchase.price_in_cents + + tax_percent = 0 + if current_user.stripe_zip_code + lookup = ZipCodes.identify(current_user.stripe_zip_code) + if lookup && lookup[:state_code] == 'TX' + tax_percent = 0.0825 + end + end + + tax_in_cents = (subtotal_in_cents * tax_percent).round + total_in_cents = subtotal_in_cents + tax_in_cents + + stripe_charge = Stripe::Charge.create( + :amount => total_in_cents, + :currency => "usd", + :customer => current_user.stripe_customer_id, + :description => target.stripe_description(lesson_booking) + ) + + sale_line_item.lesson_package_purchase = purchase + sale_line_item.save + + price_info = {} + price_info[:subtotal_in_cents] = subtotal_in_cents + price_info[:tax_in_cents] = tax_in_cents + price_info[:total_in_cents] = total_in_cents + price_info[:currency] = 'USD' + price_info[:charge_id] = stripe_charge.id + price_info[:charge] = stripe_charge + price_info[:purchase] = purchase + price_info + 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) + def self.order_jam_tracks(current_user, shopping_carts) + + shopping_carts_jam_tracks = [] + shopping_carts_subscriptions = [] + shopping_carts_gift_cards = [] + + shopping_carts.each do |shopping_cart| + if shopping_cart.is_jam_track? + shopping_carts_jam_tracks << shopping_cart + elsif shopping_cart.is_gift_card? + shopping_carts_gift_cards << shopping_cart + else + # XXX: this may have to be revisited when we actually have something other than JamTracks for puchase + raise "unknown shopping cart type #{shopping_cart.cart_type}" + shopping_carts_subscriptions << shopping_cart + end + end client = RecurlyClient.new sale = nil Sale.transaction do - sale = create_jam_track_sale(current_user) + sale = create_jam_track_sale(current_user, SOURCE_RECURLY) if sale.valid? - if is_only_freebie(shopping_carts_jam_tracks) - sale.process_jam_tracks(current_user, shopping_carts_jam_tracks, nil) + if is_only_freebie(shopping_carts) + sale.process_shopping_carts(current_user, shopping_carts, nil) sale.recurly_subtotal_in_cents = 0 sale.recurly_tax_in_cents = 0 @@ -159,11 +369,13 @@ module JamRuby return sale end - 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.sale_line_items.each do |sale_line_item| + 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 + end sale.save else @@ -173,7 +385,7 @@ module JamRuby purge_pending_adjustments(account) - created_adjustments = sale.process_jam_tracks(current_user, shopping_carts_jam_tracks, account) + created_adjustments = sale.process_shopping_carts(current_user, shopping_carts, account) # now invoice the sale ... almost done @@ -229,13 +441,13 @@ module JamRuby sale end - def process_jam_tracks(current_user, shopping_carts_jam_tracks, account) + def process_shopping_carts(current_user, shopping_carts, account) created_adjustments = [] begin - shopping_carts_jam_tracks.each do |shopping_cart| - process_jam_track(current_user, shopping_cart, account, created_adjustments) + shopping_carts.each do |shopping_cart| + process_shopping_cart(current_user, shopping_cart, account, created_adjustments) end rescue Recurly::Error, NoMethodError => x # rollback any adjustments created if error @@ -251,7 +463,7 @@ module JamRuby end - def process_jam_track(current_user, shopping_cart, account, created_adjustments) + def process_shopping_cart(current_user, shopping_cart, account, created_adjustments) recurly_adjustment_uuid = nil recurly_adjustment_credit_uuid = nil @@ -259,15 +471,19 @@ module JamRuby shopping_cart.reload # get the JamTrack in this shopping cart - jam_track = shopping_cart.cart_product + cart_product = 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 + if shopping_cart.is_jam_track? + jam_track = 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 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) @@ -300,38 +516,63 @@ module JamRuby # 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}") + @@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: current_user.id, jam_track_id: jam_track.id}) do |jam_track_right| - jam_track_right.redeemed = shopping_cart.free? - end + if shopping_cart.is_jam_track? + jam_track = cart_product - # 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) + # 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: current_user.id, jam_track_id: jam_track.id}) do |jam_track_right| + jam_track_right.redeemed = shopping_cart.free? + jam_track_right.version = jam_track.version end + + # also if the purchase was a free one, then: + # first, mark the free has_redeemable_jamtrack field if that's still true + # and if still they have more free things, then redeem the giftable_jamtracks + if shopping_cart.free? + current_user.redeem_free_credit + 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 + + # blow up the transaction if the JamTrackRight did not get created + raise RecurlyClientError.new(jam_track_right.errors) if jam_track_right.errors.any? + + elsif shopping_cart.is_gift_card? + gift_card_type = cart_product + raise "gift card is null" if gift_card_type.nil? + raise if current_user.nil? + + shopping_cart.quantity.times do |item| + gift_card_purchase = GiftCardPurchase.new( + { + user: current_user, + gift_card_type: gift_card_type + }) + + unless gift_card_purchase.save + raise RecurlyClientError.new(gift_card_purchase.errors) + end + end + + else + raise 'unknown shopping cart type: ' + shopping_cart.cart_type 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 @@ -342,7 +583,7 @@ module JamRuby 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 + }).deliver_now end end @@ -358,10 +599,24 @@ module JamRuby sale_type == JAMTRACK_SALE end - def self.create_jam_track_sale(user) + def is_lesson_sale? + sale_type == LESSON_SALE + end + + def self.create_jam_track_sale(user, sale_source=nil) sale = Sale.new sale.user = user - sale.sale_type = JAMTRACK_SALE + sale.sale_type = JAMTRACK_SALE # gift cards and jam tracks are sold with this type of sale + sale.order_total = 0 + sale.source = sale_source if sale_source + sale.save + sale + end + + def self.create_lesson_sale(user) + sale = Sale.new + sale.user = user + sale.sale_type = LESSON_SALE # gift cards and jam tracks are sold with this type of sale sale.order_total = 0 sale.save sale @@ -377,4 +632,4 @@ module JamRuby WHERE sale_type = '#{JAMTRACK_SALE}'") end end -end \ No newline at end of file +end diff --git a/ruby/lib/jam_ruby/models/sale_line_item.rb b/ruby/lib/jam_ruby/models/sale_line_item.rb index 7d744cfe5..7dec6502d 100644 --- a/ruby/lib/jam_ruby/models/sale_line_item.rb +++ b/ruby/lib/jam_ruby/models/sale_line_item.rb @@ -4,14 +4,23 @@ module JamRuby JAMBLASTER = 'JamBlaster' JAMCLOUD = 'JamCloud' JAMTRACK = 'JamTrack' + GIFTCARD = 'GiftCardType' + LESSON = 'LessonPackageType' 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 :gift_card, class_name: 'JamRuby::GiftCard' + belongs_to :lesson_package_purchase, class_name: 'JamRuby::LessonPackagePurchase' + + # deprecated; use affiliate_distribution !! belongs_to :affiliate_referral, class_name: 'JamRuby::AffiliatePartner', foreign_key: :affiliate_referral_id + + has_many :affiliate_distributions, class_name: 'JamRuby::AffiliateDistribution' + 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 :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK, GIFTCARD, LESSON]} validates :unit_price, numericality: {only_integer: false} validates :quantity, numericality: {only_integer: true} validates :free, numericality: {only_integer: true} @@ -21,17 +30,30 @@ module JamRuby validates :recurly_plan_code, presence:true validates :sale, presence:true + def is_jam_track? + product_type == JAMTRACK + end + + def is_gift_card? + product_type == GIFTCARD + end + def product if product_type == JAMTRACK JamTrack.find_by_id(product_id) + elsif product_type == GIFTCARD + GiftCardType.find_by_id(product_id) + elsif product_type == LESSON + lesson_package_purchase else + raise 'unsupported product type' end end def product_info item = product - { name: product.name } if item + { name: product.name, product_type: product_type } if item end def state @@ -61,8 +83,53 @@ module JamRuby 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 + # in a shopping-cart less world (ios purchase), let's reuse as much logic as possible + def self.create_from_jam_track(current_user, sale, jam_track, mark_redeem) + shopping_cart = ShoppingCart.create(current_user, jam_track, 1, mark_redeem) + line_item = create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + shopping_cart.destroy + line_item + end + + def add_referral_if_needed(user, shopping_cart, lesson_booking) + # if the teacher came from an affiliate, this is our chance to account for that (the student's affiliate status was accounted for in create_from_shopping_cart) + referral_info = user.should_attribute_sale?(shopping_cart, lesson_booking) + + if referral_info + self.affiliate_distributions << AffiliateDistribution.create(user.affiliate_referral, referral_info[:fee_in_cents], self) + self.save! + end + end + # in a shopping-cart less world (ios purchase), let's reuse as much logic as possible + def self.create_from_lesson_package(current_user, sale, lesson_package_type, lesson_booking) + teacher = lesson_booking.teacher if lesson_booking + shopping_cart = ShoppingCart.create(current_user, lesson_package_type, 1) + line_item = create_from_shopping_cart(sale, shopping_cart, nil, nil, nil, lesson_booking) + + if lesson_booking + + teacher = lesson_booking.teacher + student = lesson_booking.student + + if lesson_booking.is_test_drive? + # no referral for test drives + elsif lesson_booking.school_on_school? + # no referral; we don't make money on school-on-school + else + line_item.add_referral_if_needed(student, shopping_cart, lesson_booking) + + if lesson_booking.school.nil? + line_item.add_referral_if_needed(teacher, shopping_cart, lesson_booking) + end + end + end + + shopping_cart.destroy + line_item + end + + def self.create_from_shopping_cart(sale, shopping_cart, recurly_subscription_uuid, recurly_adjustment_uuid, recurly_adjustment_credit_uuid, instance = nil) + product_info = shopping_cart.product_info(instance) sale.order_total = sale.order_total + product_info[:real_price] @@ -81,9 +148,10 @@ module JamRuby # determine if we need to associate this sale with a partner user = shopping_cart.user - referral_info = user.should_attribute_sale?(shopping_cart) + referral_info = user.should_attribute_sale?(shopping_cart, instance) if !instance || !instance.is_a?(LessonBooking) # all affiliate stuff is handled elsewhere if referral_info + sale_line_item.affiliate_distributions << AffiliateDistribution.create(user.affiliate_referral, referral_info[:fee_in_cents], sale_line_item) sale_line_item.affiliate_referral = user.affiliate_referral sale_line_item.affiliate_referral_fee_in_cents = referral_info[:fee_in_cents] end diff --git a/ruby/lib/jam_ruby/models/sale_receipt_ios.rb b/ruby/lib/jam_ruby/models/sale_receipt_ios.rb new file mode 100644 index 000000000..4a311dd9a --- /dev/null +++ b/ruby/lib/jam_ruby/models/sale_receipt_ios.rb @@ -0,0 +1,7 @@ +module JamRuby + class SaleReceiptIOS < JsonStore + + belongs_to :sale, class_name: "JamRuby::Sale", foreign_key: :foreign_key1_id + + end +end diff --git a/ruby/lib/jam_ruby/models/school.rb b/ruby/lib/jam_ruby/models/school.rb new file mode 100644 index 000000000..1e7523b50 --- /dev/null +++ b/ruby/lib/jam_ruby/models/school.rb @@ -0,0 +1,122 @@ +module JamRuby + class School < ActiveRecord::Base + + include HtmlSanitize + html_sanitize strict: [:name] + + + # the school will handle all communication with students when setting up a session + SCHEDULING_COMM_SCHOOL = 'school' + # the teacher will handle all communication with students when setting up a session + SCHEDULING_COMM_TEACHER = 'teacher' + SCHEDULING_COMMS = [ SCHEDULING_COMM_SCHOOL, SCHEDULING_COMM_TEACHER ] + + attr_accessor :updating_avatar + attr_accessible :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection + + belongs_to :user, class_name: ::JamRuby::User, inverse_of: :owned_school + belongs_to :affiliate_partner, class_name: "JamRuby::AffiliatePartner" + has_many :students, class_name: "JamRuby::User" + has_many :teachers, class_name: "JamRuby::Teacher" + has_many :school_invitations, class_name: 'JamRuby::SchoolInvitation' + has_many :teacher_payments, class_name: 'JamRuby::TeacherPayment' + has_many :teacher_distributions, class_name: 'JamRuby::TeacherDistribution' + + validates :user, presence: true + validates :enabled, inclusion: {in: [true, false]} + validates :scheduling_communication, inclusion: {in: SCHEDULING_COMMS} + validates :correspondence_email, email: true, allow_blank: true + validate :validate_avatar_info + + after_create :create_affiliate + before_save :stringify_avatar_info, :if => :updating_avatar + + def scheduling_comm? + scheduling_communication == SCHEDULING_COMM_SCHOOL + end + + def communication_email + correspondence_email.blank? ? owner.email : correspondence_email + end + + def create_affiliate + AffiliatePartner.create_from_school(self) + end + def update_from_params(params) + self.name = params[:name] if params[:name].present? + self.scheduling_communication = params[:scheduling_communication] if params[:scheduling_communication].present? + self.correspondence_email = params[:correspondence_email] if params[:correspondence_email].present? + self.save + end + + def owner + user + end + + def validate_avatar_info + if updating_avatar + # we want to mak sure that original_fpfile and cropped_fpfile seems like real fpfile info objects (i.e, json objects from filepicker.io) + errors.add(:original_fpfile, ValidationMessages::INVALID_FPFILE) if self.original_fpfile.nil? || self.original_fpfile["key"].nil? || self.original_fpfile["url"].nil? + errors.add(:cropped_fpfile, ValidationMessages::INVALID_FPFILE) if self.cropped_fpfile.nil? || self.cropped_fpfile["key"].nil? || self.cropped_fpfile["url"].nil? + errors.add(:cropped_large_fpfile, ValidationMessages::INVALID_FPFILE) if self.cropped_large_fpfile.nil? || self.cropped_large_fpfile["key"].nil? || self.cropped_large_fpfile["url"].nil? + end + end + + def escape_filename(path) + dir = File.dirname(path) + file = File.basename(path) + "#{dir}/#{ERB::Util.url_encode(file)}" + end + + def update_avatar(original_fpfile, cropped_fpfile, cropped_large_fpfile, crop_selection, aws_bucket) + self.updating_avatar = true + + cropped_s3_path = cropped_fpfile["key"] + cropped_large_s3_path = cropped_large_fpfile["key"] + + self.update_attributes( + :original_fpfile => original_fpfile, + :cropped_fpfile => cropped_fpfile, + :cropped_large_fpfile => cropped_large_fpfile, + :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 => true), + :large_photo_url => S3Util.url(aws_bucket, escape_filename(cropped_large_s3_path), :secure => true) + ) + end + + def delete_avatar(aws_bucket) + + User.transaction do + + unless self.cropped_s3_path.nil? + S3Util.delete(aws_bucket, File.dirname(self.cropped_s3_path) + '/cropped.jpg') + S3Util.delete(aws_bucket, self.cropped_s3_path) + S3Util.delete(aws_bucket, self.cropped_large_s3_path) + end + + return self.update_attributes( + :original_fpfile => nil, + :cropped_fpfile => nil, + :cropped_large_fpfile => nil, + :cropped_s3_path => nil, + :cropped_large_s3_path => nil, + :photo_url => nil, + :crop_selection => nil, + :large_photo_url => nil + ) + end + end + + def stringify_avatar_info + # fpfile comes in as a hash, which is a easy-to-use and validate form. However, we store it as a VARCHAR, + # so we need t oconvert it to JSON before storing it (otherwise it gets serialized as a ruby object) + # later, when serving this data out to the REST API, we currently just leave it as a string and make a JSON capable + # client parse it, because it's very rare when it's needed at all + self.original_fpfile = original_fpfile.to_json if !original_fpfile.nil? + self.cropped_fpfile = cropped_fpfile.to_json if !cropped_fpfile.nil? + self.crop_selection = crop_selection.to_json if !crop_selection.nil? + end + end +end diff --git a/ruby/lib/jam_ruby/models/school_invitation.rb b/ruby/lib/jam_ruby/models/school_invitation.rb new file mode 100644 index 000000000..b243cdb8f --- /dev/null +++ b/ruby/lib/jam_ruby/models/school_invitation.rb @@ -0,0 +1,100 @@ +module JamRuby + class SchoolInvitation < ActiveRecord::Base + + include HtmlSanitize + html_sanitize strict: [:note] + + + belongs_to :user, class_name: ::JamRuby::User + belongs_to :school, class_name: ::JamRuby::School + + validates :school, presence: true + validates :email, email: true + validates :invitation_code, presence: true + validates :as_teacher, inclusion: {in: [true, false]} + validates :accepted, inclusion: {in: [true, false]} + validates :first_name, presence: true + validates :last_name, presence: true + validate :school_has_name, on: :create + + before_validation(on: :create) do + self.invitation_code = SecureRandom.urlsafe_base64 if self.invitation_code.nil? + end + + def school_has_name + if school && school.name.blank? + errors.add(:school, "must have name") + end + end + + def self.index(school, params) + limit = params[:per_page] + limit ||= 100 + limit = limit.to_i + + query = SchoolInvitation.where(school_id: school.id) + query = query.includes([:user, :school]) + query = query.order('created_at') + query = query.where(as_teacher: params[:as_teacher]) + query = query.where(accepted:false) + + + 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 self.create(current_user, specified_school, params) + + invitation = SchoolInvitation.new + invitation.school = specified_school + invitation.as_teacher = params[:as_teacher] + invitation.email = params[:email] + invitation.first_name = params[:first_name] + invitation.last_name = params[:last_name] + + if invitation.save + invitation.send_invitation + end + invitation + end + + + def send_invitation + if as_teacher + UserMailer.invite_school_teacher(self).deliver_now + else + UserMailer.invite_school_student(self).deliver_now + end + end + def generate_signup_url + if as_teacher + "#{APP_CONFIG.external_root_url}/school/#{school.id}/teacher?invitation_code=#{self.invitation_code}" + else + "#{APP_CONFIG.external_root_url}/school/#{school.id}/student?invitation_code=#{self.invitation_code}" + end + end + + def delete + self.destroy + end + + def resend + send_invitation + end + + + + end +end diff --git a/ruby/lib/jam_ruby/models/shopping_cart.rb b/ruby/lib/jam_ruby/models/shopping_cart.rb index ca52a2766..999bb0f98 100644 --- a/ruby/lib/jam_ruby/models/shopping_cart.rb +++ b/ruby/lib/jam_ruby/models/shopping_cart.rb @@ -12,6 +12,8 @@ module JamRuby attr_accessible :quantity, :cart_type, :product_info + attr_accessor :skip_mix_check + 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" @@ -20,12 +22,17 @@ module JamRuby validates :cart_type, presence: true validates :cart_class_name, presence: true validates :marked_for_redeem, numericality: {only_integer: true} + validate :not_mixed default_scope { order('created_at DESC') } - def product_info + def product_info(instance = nil) product = self.cart_product - {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? + data = {type: cart_type, 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, sale_display:product.sale_display} unless product.nil? + if data && instance + data.merge!(instance.product_info) + end + data end # multiply quantity by price @@ -38,6 +45,31 @@ module JamRuby (quantity - marked_for_redeem) * product.price end + def not_mixed + + return if @skip_mix_check + existing_carts = [] + this_user = any_user() + + if this_user + existing_carts = this_user.shopping_carts + end + + existing_carts = existing_carts.to_a + existing_carts << self + + if Sale.is_mixed(existing_carts) + if free? + errors.add(:base, "You can not add a free JamTrack to a cart with non-free items. Please clear out your cart.") + return false + else + errors.add(:base, "You can not add a non-free JamTrack to a cart containing free items. Please clear out your cart.") + return false + end + end + + false + end def cart_product self.cart_class_name.classify.constantize.find_by_id(self.cart_id) unless self.cart_class_name.blank? @@ -51,7 +83,18 @@ module JamRuby marked_for_redeem == quantity end + def any_user + if user + user + elsif anonymous_user_id + AnonymousUser.new(anonymous_user_id, nil) + else + nil + end + end + def self.create user, product, quantity = 1, mark_redeem = false + cart = ShoppingCart.new if user.is_a?(User) cart.user = user @@ -72,39 +115,46 @@ module JamRuby cart_type == JamTrack::PRODUCT_TYPE end + def is_gift_card? + cart_type == GiftCardType::PRODUCT_TYPE + end + + def is_lesson? + cart_type == LessonPackageType::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? + raise "not a jam track or gift card" unless is_jam_track? || is_gift_card? 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)", + description: info[:sale_display] + " (Credit)", tax_exempt: true }, { accounting_code: PURCHASE_FREE, currency: 'USD', unit_amount_in_cents: (info[:total_price] * 100).to_i, - description: "JamTrack: " + info[:name], + description: info[:sale_display], tax_exempt: true } ] else + [ { accounting_code: PURCHASE_NORMAL, currency: 'USD', unit_amount_in_cents: (info[:total_price] * 100).to_i, - description: "JamTrack: " + info[:name], + description: info[:sale_display], tax_exempt: false } ] @@ -113,8 +163,13 @@ module JamRuby 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) + if shopping_cart.is_jam_track? + mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(user) + cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, mark_redeem) + else + cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, false) + end + end anonymous_user.destroy_all_shopping_carts @@ -134,28 +189,32 @@ module JamRuby # if no shpping carts have been marked, then mark it redeemable # should be wrapped in a TRANSACTION def self.user_has_redeemable_jam_track?(any_user) - mark_redeem = false - if APP_CONFIG.one_free_jamtrack_per_user && any_user.has_redeemable_jamtrack - mark_redeem = true # start out assuming we can redeem... + + if any_user.has_redeemable_jamtrack || any_user.gifted_jamtracks > 0 + + free_in_cart = 0 any_user.shopping_carts.each do |shopping_cart| # but if we find any shopping cart item already marked for redeem, then back out of mark_redeem=true - if shopping_cart.cart_type == JamTrack::PRODUCT_TYPE && shopping_cart.marked_for_redeem > 0 - mark_redeem = false - break + if shopping_cart.cart_type == JamTrack::PRODUCT_TYPE + free_in_cart += shopping_cart.marked_for_redeem end end + + any_user.free_jamtracks > free_in_cart + else + false end - mark_redeem end # adds a jam_track to cart, checking for promotions - def self.add_jam_track_to_cart(any_user, jam_track) + def self.add_jam_track_to_cart(any_user, jam_track, clear:false) cart = nil ShoppingCart.transaction do - 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 + if clear + # if you are an anonymous user, we make sure there is nothing else in your shopping cart ... keep it clean for the 'new user rummaging around for a freebie scenario' + any_user.destroy_jam_track_shopping_carts + any_user.reload end mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user) @@ -164,23 +223,66 @@ module JamRuby cart end + def self.add_item_to_cart(any_user, item) + cart = nil + ShoppingCart.transaction do + cart = ShoppingCart.create(any_user, item, 1, false) + end + cart + end + # deletes a jam track from the shopping cart, updating redeem flag as necessary def self.remove_jam_track_from_cart(any_user, cart) ShoppingCart.transaction do cart.destroy - # check if we should move the redemption + + # so that user.shopping_carts reflects truth + any_user.reload + + # check if we should move the redemption around automatically mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user) carts = any_user.shopping_carts - # if we find any carts on the account, mark one redeemable + # if we find any carts on the account that are not redeemed, mark first one redeemable if mark_redeem && carts.length > 0 - carts[0].redeem(mark_redeem) - carts[0].save + carts.each do |cart| + if cart.marked_for_redeem == 0 + if cart.quantity > 1 + raise 'unknown situation for redeemption juggling' + end + cart.redeem(mark_redeem) + cart.save + break + end + end end end end + def self.remove_item_from_cart(any_user, cart) + ShoppingCart.transaction do + cart.destroy + end + end + + # if the number of items in the shopping cart is less than gifted_jamtracks on the user, then fix them all up + def self.apply_gifted_jamtracks(user) + jam_track_carts = user.shopping_carts.where(cart_type:JamTrack::PRODUCT_TYPE) + + if jam_track_carts.count > user.gifted_jamtracks + # just whack everything in their shopping cart + user.destroy_all_shopping_carts + return + end + + jam_track_carts.each do |cart| + cart.skip_mix_check = true + cart.marked_for_redeem = 1 + cart.save! + end + end + def port(user, anonymous_user) ShoppingCart.transaction do diff --git a/ruby/lib/jam_ruby/models/signup_hint.rb b/ruby/lib/jam_ruby/models/signup_hint.rb index c95353d6c..05ef88c1f 100644 --- a/ruby/lib/jam_ruby/models/signup_hint.rb +++ b/ruby/lib/jam_ruby/models/signup_hint.rb @@ -13,6 +13,16 @@ module JamRuby validates :redirect_location, length: {maximum: 1000} validates :want_jamblaster, inclusion: {in: [nil, true, false]} + def self.create_redirect(user, options = {}) + hint = SignupHint.new + hint.user = user + hint.redirect_location = options[:redirect_location] if options.has_key?(:redirect_location) + hint.want_jamblaster = false + hint.expires_at = 2.days.from_now + hint.save + hint + end + def self.refresh_by_anoymous_user(anonymous_user, options = {}) hint = SignupHint.find_by_anonymous_user_id(anonymous_user.id) @@ -34,5 +44,28 @@ module JamRuby SignupHint.where("created_at < :week", {:week => 1.week.ago}).delete_all end + def self.most_recent_redirect(user, default, queryParams=nil) + puts "jquery params" + hint = SignupHint.where(user_id: user.id).order('created_at desc').first + + if hint + redirect = hint.redirect_location + puts "redirect #{redirect}" + uri = URI.parse(redirect) + bits = uri.query ? URI.decode_www_form(uri.query) : [] + if queryParams + queryParams.each do |k, v| + bits << [k, v] + end + end + + puts "bits #{bits}" + uri.query = URI.encode_www_form(bits) + puts "oh yeah #{uri.to_s}" + return uri.to_s + else + default + end + end end end diff --git a/ruby/lib/jam_ruby/models/subject.rb b/ruby/lib/jam_ruby/models/subject.rb new file mode 100644 index 000000000..a99a9745d --- /dev/null +++ b/ruby/lib/jam_ruby/models/subject.rb @@ -0,0 +1,8 @@ +module JamRuby + class Subject < ActiveRecord::Base + include HtmlSanitize + html_sanitize strict: [:name, :description] + has_many :teachers, :class_name => "JamRuby::Teacher", :through => :teachers_subjects + has_many :teachers_subjects, class_name: "JamRuby::TeacherSubject" + end +end diff --git a/ruby/lib/jam_ruby/models/teacher.rb b/ruby/lib/jam_ruby/models/teacher.rb new file mode 100644 index 000000000..c660dcd38 --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher.rb @@ -0,0 +1,440 @@ +module JamRuby + class Teacher < ActiveRecord::Base + include HtmlSanitize + html_sanitize strict: [:biography, :website] + attr_accessor :validate_introduction, :validate_basics, :validate_pricing + attr_accessible :genres, :teacher_experiences, :experiences_teaching, :experiences_education, :experiences_award + has_many :genres, :class_name => "JamRuby::Genre", :through => :teachers_genres # , :order => "description" + has_many :teachers_genres, :class_name => "JamRuby::TeacherGenre" + has_many :instruments, :class_name => "JamRuby::Instrument", through: :teachers_instruments # , :order => "description" + has_many :teachers_instruments, class_name: "JamRuby::TeacherInstrument" + has_many :subjects, :class_name => "JamRuby::Subject", :through => :teachers_subjects # , :order => "description" + has_many :teachers_subjects, class_name: "JamRuby::TeacherSubject" + has_many :languages, :class_name => "JamRuby::Language", :through => :teachers_languages # , :order => "description" + has_many :teachers_languages, class_name: "JamRuby::TeacherLanguage" + has_many :teacher_experiences, :class_name => "JamRuby::TeacherExperience" + has_many :experiences_teaching, -> {where(experience_type: 'teaching')}, :class_name => "JamRuby::TeacherExperience" + has_many :experiences_education, -> {where(experience_type: 'education')}, :class_name => "JamRuby::TeacherExperience" + has_many :experiences_award, -> {where(experience_type: 'award')}, :class_name => "JamRuby::TeacherExperience" + has_many :reviews, :class_name => "JamRuby::Review", as: :target + has_many :lesson_sessions, :class_name => "JamRuby::LessonSession" + has_many :lesson_package_purchases, :class_name => "JamRuby::LessonPackagePurchase" + has_one :review_summary, :class_name => "JamRuby::ReviewSummary", as: :target + has_one :user, :class_name => 'JamRuby::User', foreign_key: :teacher_id + belongs_to :school, :class_name => "JamRuby::School", inverse_of: :teachers + + validates :user, :presence => true + validates :biography, length: {minimum: 5, maximum: 4096}, :if => :validate_introduction + validates :introductory_video, :format => {:with => /A(?:https?:\/\/)?(?:www\.)?youtu(?:\.be|be\.com)\/(?:watch\?v=)?([\w-]{10,})z/, message: "is not a valid youtube URL"}, :allow_blank => true, :if => :validate_introduction + validates :years_teaching, :presence => true, :if => :validate_introduction + validates :years_playing, :presence => true, :if => :validate_introduction + validates :teaches_test_drive, inclusion: {in: [true, false]}, :if => :validate_pricing + validates :top_rated, inclusion: {in: [true, false]} + validates :test_drives_per_week, numericality: {only_integer: true, minimum: 2, maximum: 10}, :if => :validate_pricing + validates :instruments, :length => {minimum: 1, message: "At least one instrument or subject is required"}, if: :validate_basics, unless: ->(teacher) { teacher.subjects.length>0 } + validates :subjects, :length => {minimum: 1, message: "At least one instrument or subject is required"}, if: :validate_basics, unless: ->(teacher) { teacher.instruments.length>0 } + validates :genres, :length => {minimum: 1, message: "At least one genre is required"}, if: :validate_basics + validates :languages, :length => {minimum: 1, message: "At least one language is required"}, if: :validate_basics + + validate :offer_pricing, :if => :validate_pricing + validate :offer_duration, :if => :validate_pricing + validate :teaches_ages, :if => :validate_basics + + #default_scope { includes(:genres).order('created_at desc') } + + after_save :update_profile_pct + + def update_profile_pct + result = pct_complete + self.profile_pct = result[:pct] + self.profile_pct_summary = result.to_json + Teacher.where(id: id).update_all(profile_pct: self.profile_pct, profile_pct_summary: self.profile_pct_summary) + end + + + def self.index(user, params = {}) + limit = params[:per_page] + limit ||= 20 + limit = limit.to_i + + query = User.unscoped.joins(:teacher) + + # only show teachers with ready for session set to true + query = query.where('teachers.ready_for_session_at IS NOT NULL') + + if user && params[:onlyMySchool] && params[:onlyMySchool] != 'false' && user.school_id + query = query.where("teachers.school_id = ?", user.school_id) + end + + instruments = params[:instruments] + if instruments && !instruments.blank? && instruments.length > 0 + query = query.joins("inner JOIN teachers_instruments AS tinst ON tinst.teacher_id = teachers.id") + .where("tinst.instrument_id IN (?)", instruments) + end + + subjects = params[:subjects] + if subjects && !subjects.blank? && subjects.length > 0 + query = query.joins("inner JOIN teachers_subjects AS tsubjs ON tsubjs.teacher_id = teachers.id") + .where('tsubjs.subject_id IN (?)', subjects) + end + + genres = params[:genres] + if genres && !genres.blank? && genres.length > 0 + query = query.joins("inner JOIN teachers_genres AS tgenres ON tgenres.teacher_id = teachers.id") + .where('tgenres.genre_id IN (?)', genres) + end + + country = params[:country] + if country && country.length > 0 + query = query.where(country: country) + end + + region = params[:region] + if region && region.length > 0 + query = query.where(state: region) + end + + languages = params[:languages] + if languages && !languages.blank? && languages.length > 0 + query= query.joins("inner JOIN teachers_languages AS tlang ON tlang.teacher_id = teachers.id") + .where('tlang.language_id IN (?)', languages) + end + + years_teaching = params[:years_teaching].to_i + if params[:years_teaching] && years_teaching > 0 + query = query.where('years_teaching >= ?', years_teaching) + end + + teaches_beginner = params[:teaches_beginner] + teaches_intermediate = params[:teaches_intermediate] + teaches_advanced = params[:teaches_advanced] + + if teaches_beginner.present? || teaches_intermediate.present? || teaches_advanced.present? + + clause = '' + + if teaches_beginner == true + clause << 'teaches_beginner = true' + end + if teaches_intermediate == true + if clause.length > 0 + clause << ' OR ' + end + clause << 'teaches_intermediate = true' + end + if teaches_advanced == true + if clause.length > 0 + clause << ' OR ' + end + clause << 'teaches_advanced = true' + end + query = query.where(clause) + end + + student_age = params[:student_age].to_i + if params[:student_age] && student_age > 0 + query = query.where("teaches_age_lower <= ? AND (CASE WHEN teaches_age_upper = 0 THEN true ELSE teaches_age_upper >= ? END)", student_age, student_age) + end + + # order in this way: https://jamkazam.atlassian.net/browse/VRFS-4058 + # Real teachers who are marked as top. + # Real teachers who are not marked as top. + # Phantom teachers. + + query = query.order("top_rated DESC, phantom ASC") + + 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 self.save_teacher(user, params) + teacher = build_teacher(user, params) + if teacher.save + # flag the user as a teacher + teacher.user.is_a_teacher = true + teacher.user.save(validate: false) + end + teacher + end + + def self.build_teacher(user, params) + # ensure person creating this Teacher is a Musician + unless user && user.musician? + raise JamPermissionError, "must be a musician" + end + + teacher = user.teacher + teacher ||= Teacher.new + teacher.user = user + + teacher.website = params[:website] if params.key?(:website) + teacher.biography = params[:biography] if params.key?(:biography) + teacher.introductory_video = params[:introductory_video] if params.key?(:introductory_video) + teacher.years_teaching = params[:years_teaching] if params.key?(:years_teaching) + teacher.years_playing = params[:years_playing] if params.key?(:years_playing) + teacher.teaches_age_lower = params[:teaches_age_lower] if params.key?(:teaches_age_lower) + teacher.teaches_age_upper = params[:teaches_age_upper] if params.key?(:teaches_age_upper) + teacher.website = params[:website] if params.key?(:website) + teacher.biography = params[:biography] if params.key?(:biography) + teacher.teaches_beginner = params[:teaches_beginner] if params.key?(:teaches_beginner) + teacher.teaches_intermediate = params[:teaches_intermediate] if params.key?(:teaches_intermediate) + teacher.teaches_advanced = params[:teaches_advanced] if params.key?(:teaches_advanced) + teacher.prices_per_lesson = params[:prices_per_lesson] if params.key?(:prices_per_lesson) + teacher.prices_per_month = params[:prices_per_month] if params.key?(:prices_per_month) + teacher.lesson_duration_30 = params[:lesson_duration_30] if params.key?(:lesson_duration_30) + teacher.lesson_duration_45 = params[:lesson_duration_45] if params.key?(:lesson_duration_45) + teacher.lesson_duration_60 = params[:lesson_duration_60] if params.key?(:lesson_duration_60) + teacher.lesson_duration_90 = params[:lesson_duration_90] if params.key?(:lesson_duration_90) + teacher.lesson_duration_120 = params[:lesson_duration_120] if params.key?(:lesson_duration_120) + teacher.price_per_lesson_30_cents = params[:price_per_lesson_30_cents] if params.key?(:price_per_lesson_30_cents) + teacher.price_per_lesson_45_cents = params[:price_per_lesson_45_cents] if params.key?(:price_per_lesson_45_cents) + teacher.price_per_lesson_60_cents = params[:price_per_lesson_60_cents] if params.key?(:price_per_lesson_60_cents) + teacher.price_per_lesson_90_cents = params[:price_per_lesson_90_cents] if params.key?(:price_per_lesson_90_cents) + teacher.price_per_lesson_120_cents = params[:price_per_lesson_120_cents] if params.key?(:price_per_lesson_120_cents) + teacher.price_per_month_30_cents = params[:price_per_month_30_cents] if params.key?(:price_per_month_30_cents) + teacher.price_per_month_45_cents = params[:price_per_month_45_cents] if params.key?(:price_per_month_45_cents) + teacher.price_per_month_60_cents = params[:price_per_month_60_cents] if params.key?(:price_per_month_60_cents) + teacher.price_per_month_90_cents = params[:price_per_month_90_cents] if params.key?(:price_per_month_90_cents) + teacher.price_per_month_120_cents = params[:price_per_month_120_cents] if params.key?(:price_per_month_120_cents) + teacher.teaches_test_drive = params[:teaches_test_drive] if params.key?(:teaches_test_drive) + teacher.test_drives_per_week = params[:test_drives_per_week] if params.key?(:test_drives_per_week) + teacher.test_drives_per_week = 10 if !params.key?(:test_drives_per_week) # default to 10 in absence of others + teacher.school_id = params[:school_id] if params.key?(:school_id) + + + # Many-to-many relations: + if params.key?(:genres) + genres = params[:genres] + genres = [] if genres.nil? + teacher.genres = genres.collect { |genre_id| Genre.find(genre_id) } + end + if params.key?(:instruments) + instruments = params[:instruments] + instruments = [] if instruments.nil? + teacher.instruments = instruments.collect { |instrument_id| Instrument.find(instrument_id) } + end + if params.key?(:subjects) + subjects = params[:subjects] + subjects = [] if subjects.nil? + teacher.subjects = subjects.collect { |subject_id| Subject.find(subject_id) } + end + if params.key?(:languages) + languages = params[:languages] + languages = [] if languages.nil? + teacher.languages = languages.collect { |language_id| Language.find(language_id) } + end + + # Experience: + [:teaching, :education, :award].each do |experience_type| + key = "experiences_#{experience_type}".to_sym + if params.key?(key) + list = params[key] + list = [] if list.nil? + experiences = list.collect do |exp| + TeacherExperience.new( + name: exp[:name], + experience_type: experience_type, + organization: exp[:organization], + start_year: exp[:start_year], + end_year: exp[:end_year] + ) + end # collect + + # we blindly destroy/recreate on every resubmit + previous = teacher.send("#{key.to_s}") + previous.destroy_all + + # Dynamically call the appropriate method (just setting the + # value doesn't result in the behavior we need) + teacher.send("#{key.to_s}=", experiences) + end # if + end # do + + # How to validate: + teacher.validate_introduction = !!params[:validate_introduction] + teacher.validate_basics = !!params[:validate_basics] + teacher.validate_pricing = !!params[:validate_pricing] + + return teacher + end + + def booking_price(lesson_length, single) + price = nil + if single + price = self["price_per_lesson_#{lesson_length}_cents"] + else + price = self["price_per_month_#{lesson_length}_cents"] + end + + if !price.nil? + price / 100.0 + else + price + end + end + + def offer_pricing + unless prices_per_lesson.present? || prices_per_month.present? + errors.add(:offer_pricing, "Must choose to price per lesson or per month") + end + end + + def offer_duration + unless lesson_duration_30.present? || lesson_duration_45.present? || lesson_duration_60.present? || lesson_duration_90.present? || lesson_duration_120.present? + errors.add(:offer_duration, "Must offer at least one duration") + end + end + + def teaches_ages + if teaches_age_lower > 0 && teaches_age_upper > 0 && (teaches_age_upper < teaches_age_lower) + errors.add(:ages_taught, "Age range is backwards") + end + end + + def recent_reviews + reviews.order('created_at desc').limit(20) + end + + def mark_background_checked + self.background_check_at = Time.now + self.save! + end + + def mark_session_ready + self.ready_for_session_at = Time.now + self.save! + end + + def mark_top_rated + self.top_rated = true + self.save! + end + + def mark_not_top_rated + self.top_rated = false + self.save! + end + def has_experiences_teaching? + experiences_teaching.count > 0 + end + + def has_experiences_education? + experiences_education.count > 0 + end + + def has_experiences_award? + experiences_award.count > 0 + end + + def has_stripe_billing? + user.has_stripe_connect? + end + + def has_instruments_or_subject? + instruments.count > 0 || subjects.count > 0 + end + + def has_genres? + genres.count > 0 + end + + def has_languages? + languages.count > 0 + end + + def teaches_ages_specified? + (!teaches_age_lower.nil? && teaches_age_lower > 0) || (!teaches_age_upper.nil? && teaches_age_upper > 0) + end + + def teaching_level_specified? + teaches_beginner || teaches_intermediate || teaches_advanced + end + + def has_pricing_specified? + specified = false + durations_allowed = [] + [30, 45, 60, 90, 120].each do |i| + durations_allowed << i if self["lesson_duration_#{i}"] + end + + durations_allowed.each do |i| + if self["price_per_lesson_#{i}_cents"] || self["price_per_month_#{i}_cents"] + specified = true + break + end + end + specified + end + + def has_name_specified? + !user.anonymous? + end + + def stripe_account_id + user.stripe_auth.uid if user.has_stripe_connect? + end + + ## !!!! this is only valid for tests + def stripe_account_id=(new_acct_id) + existing = user.stripe_auth + existing.destroy if existing + + user_auth_hash = { + :provider => 'stripe_connect', + :uid => new_acct_id, + :token => 'bogus', + :refresh_token => 'refresh_bogus', + :token_expiration => Date.new(2050, 1, 1), + :secret => "secret" + } + + authorization = user.user_authorizations.build(user_auth_hash) + authorization.save! + + end + + # how complete is their profile? + def pct_complete + @part_complete ||= { + name_specified: has_name_specified?, + experiences_teaching: has_experiences_teaching?, + experiences_education: has_experiences_education?, + experiences_award: has_experiences_award?, + has_stripe_account: has_stripe_billing?, + has_teacher_bio: !biography.nil?, + intro_video: !introductory_video.nil?, + years_teaching: years_teaching > 0, + years_playing: years_playing > 0, + instruments_or_subject: has_instruments_or_subject?, + genres: genres.count > 0, + languages: languages.count > 0, + teaches_ages_specified: teaches_ages_specified?, + teaching_level_specified: teaching_level_specified?, + has_pricing_specified: has_pricing_specified? + } + + done = 0 + @part_complete.each do |k, v| + if v + done += 1 + end + end + + complete = 100.0 * done.to_f / @part_complete.length.to_f + + @part_complete[:pct] = complete.round + @part_complete + end + end +end diff --git a/ruby/lib/jam_ruby/models/teacher_distribution.rb b/ruby/lib/jam_ruby/models/teacher_distribution.rb new file mode 100644 index 000000000..0b82910b7 --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher_distribution.rb @@ -0,0 +1,162 @@ +module JamRuby + class TeacherDistribution < ActiveRecord::Base + + belongs_to :teacher, class_name: "JamRuby::User", foreign_key: "teacher_id" + belongs_to :teacher_payment, class_name: "JamRuby::TeacherPayment" + belongs_to :lesson_session, class_name: "JamRuby::LessonSession" + belongs_to :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase" + belongs_to :school, class_name: "JamRuby::School" + + validates :teacher, presence: true + validates :amount_in_cents, presence: true + + def self.index(current_user, params) + limit = params[:per_page] + limit ||= 100 + limit = limit.to_i + + query = TeacherDistribution.where(teacher_id: current_user.id).where('school_id IS NULL').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 + + def not_collectable + if is_test_drive? + false + elsif is_normal? + !lesson_session.billing_should_retry + else + !lesson_package_purchase.billing_should_retry + end + end + + def self.create_for_lesson(lesson_session) + distribution = create(lesson_session) + distribution.lesson_session = lesson_session + distribution + end + + def self.create_for_lesson_package_purchase(lesson_package_purchase) + distribution = create(lesson_package_purchase) + distribution.lesson_package_purchase = lesson_package_purchase + distribution + end + + def self.create(target) + distribution = TeacherDistribution.new + distribution.teacher = target.teacher + distribution.ready = false + distribution.distributed = false + distribution.amount_in_cents = target.lesson_booking.distribution_price_in_cents(target) + distribution.school = target.lesson_booking.school + distribution + end + + def target + if lesson_session + lesson_session + else + lesson_package_purchase + end + end + + def amount + amount_in_cents / 100.0 + end + + def real_distribution_in_cents + amount_in_cents - calculate_teacher_fee + end + + def real_distribution + (real_distribution_in_cents / 100.0) + end + + def real_distribution_display + '$%.2f' % real_distribution + end + + def jamkazam_margin_in_cents + if is_test_drive? + 0 + else + if school + # if school exists, use it's rate + rate = school.jamkazam_rate + else + # otherwise use the teacher's rate + rate = teacher.teacher.jamkazam_rate + end + amount_in_cents * (rate) + end + end + + def jamkazam_margin + (jamkazam_margin_in_cents / 100).round(2) + end + + def calculate_teacher_fee + if is_test_drive? + 0 + else + if school + # if school exists, use it's rate + rate = school.jamkazam_rate + else + # otherwise use the teacher's rate + rate = teacher.teacher.jamkazam_rate + end + (amount_in_cents * (rate + 0.03)).round + end + end + + def student + if lesson_session + lesson_session.student + else + lesson_package_purchase.student + end + end + + def month_name + lesson_package_purchase.month_name + end + + def is_test_drive? + lesson_session && lesson_session.is_test_drive? + end + + def is_normal? + lesson_session && !lesson_session.is_test_drive? + end + + def is_monthly? + !lesson_package_purchase.nil? + end + + def description + begin + if lesson_session + lesson_session.timed_description + else + lesson_package_purchase.timed_description + end + rescue + "temp fix" + end + + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/teacher_experience.rb b/ruby/lib/jam_ruby/models/teacher_experience.rb new file mode 100644 index 000000000..7289daa3b --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher_experience.rb @@ -0,0 +1,12 @@ +module JamRuby + class TeacherExperience < ActiveRecord::Base + include HtmlSanitize + html_sanitize strict: [:name, :organization] + belongs_to :teacher, :class_name => "JamRuby::Teacher" + attr_accessible :name, :experience_type, :organization, :start_year, :end_year + + scope :teaching, -> { where(experience_type: 'teaching')} + scope :education, -> { where(experience_type: 'education') } + scope :awards, -> { where(experience_type: 'award') } + end +end diff --git a/ruby/lib/jam_ruby/models/teacher_genre.rb b/ruby/lib/jam_ruby/models/teacher_genre.rb new file mode 100644 index 000000000..549219dff --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher_genre.rb @@ -0,0 +1,11 @@ +module JamRuby + class TeacherGenre < ActiveRecord::Base + self.table_name = "teachers_genres" + + belongs_to :teacher, class_name: "JamRuby::Teacher" + belongs_to :genre, class_name: "JamRuby::Genre" + + validates :teacher, presence:true + validates :genre, presence: true + end +end diff --git a/ruby/lib/jam_ruby/models/teacher_instrument.rb b/ruby/lib/jam_ruby/models/teacher_instrument.rb new file mode 100644 index 000000000..f1f60db41 --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher_instrument.rb @@ -0,0 +1,11 @@ +module JamRuby + class TeacherInstrument < ActiveRecord::Base + self.table_name = "teachers_instruments" + + belongs_to :teacher, class_name: "JamRuby::Teacher" + belongs_to :instrument, class_name: "JamRuby::Instrument" + + validates :teacher, presence:true + validates :instrument, presence: true + end +end diff --git a/ruby/lib/jam_ruby/models/teacher_intent.rb b/ruby/lib/jam_ruby/models/teacher_intent.rb new file mode 100644 index 000000000..333cf5afe --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher_intent.rb @@ -0,0 +1,24 @@ +module JamRuby + class TeacherIntent < ActiveRecord::Base + + belongs_to :user, class_name: ::JamRuby::User + belongs_to :teacher, class_name: ::JamRuby::Teacher + + validates :user, presence: true + validates :teacher, presence: true + validates :intent, presence: true + + def self.create(user, teacher, intent) + teacher_intent = TeacherIntent.new + teacher_intent.user = user + teacher_intent.teacher = teacher + teacher_intent.intent = intent + teacher_intent.save + teacher_intent + end + + def self.recent_test_drive(user) + TeacherIntent.where(intent: 'book-test-drive').where(user_id: user.id).where('created_at > ?', Date.today - 30).order('created_at DESC').first + end + end +end diff --git a/ruby/lib/jam_ruby/models/teacher_language.rb b/ruby/lib/jam_ruby/models/teacher_language.rb new file mode 100644 index 000000000..f0cfcf3d1 --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher_language.rb @@ -0,0 +1,11 @@ +module JamRuby + class TeacherLanguage < ActiveRecord::Base + self.table_name = "teachers_languages" + + belongs_to :teacher, class_name: "JamRuby::Teacher" + belongs_to :language, class_name: "JamRuby::Language" + + validates :teacher, presence:true + validates :language, presence: true + end +end diff --git a/ruby/lib/jam_ruby/models/teacher_payment.rb b/ruby/lib/jam_ruby/models/teacher_payment.rb new file mode 100644 index 000000000..94f2df67c --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher_payment.rb @@ -0,0 +1,118 @@ +module JamRuby + class TeacherPayment < ActiveRecord::Base + + belongs_to :teacher, class_name: "JamRuby::User", foreign_key: :teacher_id + belongs_to :teacher_payment_charge, class_name: "JamRuby::TeacherPaymentCharge", foreign_key: :charge_id + has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution" + belongs_to :school, class_name: "JamRuby::School" + + + def self.hourly_check + teacher_payments + end + + # pay the school if the payment owns the school; otherwise default to the teacher + def payable_teacher + if school + school.owner + else + teacher + end + end + + def teacher_distributions + [teacher_distribution] + end + + def self.pending_teacher_payments + User.select(['users.id']).joins(:teacher).joins(:user_authorizations).joins(:teacher_distributions).where("user_authorizations.provider = 'stripe_connect' AND user_authorizations.token_expiration > NOW()").where('teacher_distributions.distributed = false').where('teacher_distributions.ready = true').uniq + end + + def self.teacher_payments + pending_teacher_payments.each do |row| + teacher = User.find(row['id']) + + TeacherDistribution.where(teacher_id: teacher.id).where(ready:true).where(distributed: false).each do |distribution| + payment = TeacherPayment.charge(teacher) + if payment.nil? || !payment.teacher_payment_charge.billed + break + end + end + + end + end + + def amount + amount_in_cents / 100.0 + end + + def is_card_declined? + teacher_payment_charge.is_card_declined? + end + + def is_card_expired? + teacher_payment_charge.is_card_expired? + end + + def last_billed_at_date + teacher_payment_charge.last_billed_at_date + end + def charge_retry_hours + 24 + end + + def real_distribution_in_cents + amount_in_cents - fee_in_cents + end + + # will find, for a given teacher, an outstading unsuccessful payment or make a new one. + # it will then associate a charge with it, and then execute the charge. + def self.charge(teacher) + payment = TeacherPayment.joins(:teacher_payment_charge).where('teacher_payments.teacher_id = ?', teacher.id).where('charges.billed = false').order(:created_at).first + if payment.nil? + payment = TeacherPayment.new + payment.teacher = teacher + else + payment = TeacherPayment.find(payment.id) + end + + if payment.teacher_distribution.nil? + teacher_distribution = TeacherDistribution.where(teacher_id: teacher.id).where(ready:true).where(distributed: false).order(:created_at).first + if teacher_distribution.nil? + return + end + payment.teacher_distribution = teacher_distribution + end + + payment.school = payment.teacher_distribution.school + payment.amount_in_cents = payment.teacher_distribution.amount_in_cents + payment.fee_in_cents = payment.teacher_distribution.calculate_teacher_fee + + if payment.teacher_payment_charge.nil? + charge = TeacherPaymentCharge.new + charge.user = payment.payable_teacher + charge.amount_in_cents = (payment.amount_in_cents / (1 - APP_CONFIG.stripe[:ach_pct])).round + charge.fee_in_cents = payment.fee_in_cents + charge.teacher_payment = payment + payment.teacher_payment_charge = charge + # charge.save! + else + charge = payment.teacher_payment_charge + charge.amount_in_cents = (payment.amount_in_cents / (1 - APP_CONFIG.stripe[:ach_pct])).round + charge.fee_in_cents = payment.fee_in_cents + charge.save! + end + + payment.save! + + payment.teacher_payment_charge.charge + + if payment.teacher_payment_charge.billed + payment.teacher_distribution.distributed = true + payment.teacher_distribution.save! + end + payment + end + end + +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/teacher_payment_charge.rb b/ruby/lib/jam_ruby/models/teacher_payment_charge.rb new file mode 100644 index 000000000..638ef2f91 --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher_payment_charge.rb @@ -0,0 +1,57 @@ +module JamRuby + class TeacherPaymentCharge < Charge + + has_one :teacher_payment, class_name: "JamRuby::TeacherPayment", foreign_key: :charge_id + + def distribution + @distribution ||= teacher_payment.teacher_distribution + end + + def max_retries + 9999999 + end + + def teacher + @teacher ||= teacher_payment.payable_teacher + end + + def charged_user + teacher + end + + def do_charge(force) + + # source will let you supply a token. But... how to get a token in this case? + + stripe_charge = Stripe::Charge.create( + :amount => amount_in_cents, + :currency => "usd", + :customer => APP_CONFIG.stripe[:source_customer], + :description => construct_description, + :destination => teacher.teacher.stripe_account_id, + :application_fee => fee_in_cents, + ) + + stripe_charge + end + + def do_send_notices + unless teacher_payment.school && distribution.is_monthly? + # we don't send monthly success notices to the teacher if they are in a school, otherwise they get an email + UserMailer.teacher_distribution_done(teacher_payment).deliver_now + end + if teacher_payment.school + UserMailer.school_distribution_done(teacher_payment).deliver_now + end + end + + def do_send_unable_charge + UserMailer.teacher_distribution_fail(teacher_payment).deliver_now + end + + def construct_description + teacher_payment.teacher_distribution.description + end + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/teacher_subject.rb b/ruby/lib/jam_ruby/models/teacher_subject.rb new file mode 100644 index 000000000..953ff9fd2 --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher_subject.rb @@ -0,0 +1,11 @@ +module JamRuby + class TeacherSubject < ActiveRecord::Base + self.table_name = "teachers_subjects" + + belongs_to :teacher, class_name: "JamRuby::Teacher" + belongs_to :subject, class_name: "JamRuby::Subject" + + validates :teacher, presence:true + validates :subject, presence: true + end +end diff --git a/ruby/lib/jam_ruby/models/test_drive_package.rb b/ruby/lib/jam_ruby/models/test_drive_package.rb new file mode 100644 index 000000000..850ca6057 --- /dev/null +++ b/ruby/lib/jam_ruby/models/test_drive_package.rb @@ -0,0 +1,26 @@ +# represenst the type of lesson package +module JamRuby + class TestDrivePackage < ActiveRecord::Base + + @@log = Logging.logger[TestDrivePackage] + + attr_accessible :name, :description, :package_type, :test_drive_package_teachers_attributes, :test_drive_package_teachers, as: :admin + + has_many :test_drive_package_teachers, class_name: "JamRuby::TestDrivePackageTeacher" + + validates :name, presence: true, uniqueness: true + validates :package_type, presence: true + + #validate :teacher_count + + + def teacher_count + if package_type != test_drive_package_teachers.length + self.errors.add(:test_drive_package_teachers, "wrong number of teachers specified for the given package type #{package_type}") + end + end + + accepts_nested_attributes_for :test_drive_package_teachers, allow_destroy: true + end +end + diff --git a/ruby/lib/jam_ruby/models/test_drive_package_choice.rb b/ruby/lib/jam_ruby/models/test_drive_package_choice.rb new file mode 100644 index 000000000..5c41c7bba --- /dev/null +++ b/ruby/lib/jam_ruby/models/test_drive_package_choice.rb @@ -0,0 +1,17 @@ +# when a user picks a package, we mark which teachers they actually went with from the package +module JamRuby + class TestDrivePackageChoice < ActiveRecord::Base + + @@log = Logging.logger[TestDrivePackageChoice] + + belongs_to :test_drive_package, class_name: "JamRuby::TestDrivePackage" + belongs_to :user, class_name: "JamRuby::User", foreign_key: :user_id, inverse_of: :test_drive_package_choices + has_many :test_drive_package_choice_teachers, class_name: "JamRuby::TestDrivePackageChoiceTeacher", inverse_of: :test_drive_package_choice + has_many :lesson_bookings, class_name: "JamRuby::LessonBooking" + end + + def lesson_package_type + LessonPackageType.package_for_test_drive_count(test_drive_package_choice_teachers.count) + end +end + diff --git a/ruby/lib/jam_ruby/models/test_drive_package_choice_teacher.rb b/ruby/lib/jam_ruby/models/test_drive_package_choice_teacher.rb new file mode 100644 index 000000000..356880073 --- /dev/null +++ b/ruby/lib/jam_ruby/models/test_drive_package_choice_teacher.rb @@ -0,0 +1,12 @@ +# when a user picks a package, we mark which teachers they actually went with from the package +module JamRuby + class TestDrivePackageChoiceTeacher < ActiveRecord::Base + + @@log = Logging.logger[TestDrivePackageChoiceTeacher] + + belongs_to :test_drive_package_choice, class_name: "JamRuby::TestDrivePackageChoice", inverse_of: :test_drive_package_choice_teachers + belongs_to :teacher, class_name: "JamRuby::User", foreign_key: :teacher_id + + end +end + diff --git a/ruby/lib/jam_ruby/models/test_drive_package_teacher.rb b/ruby/lib/jam_ruby/models/test_drive_package_teacher.rb new file mode 100644 index 000000000..2b0208189 --- /dev/null +++ b/ruby/lib/jam_ruby/models/test_drive_package_teacher.rb @@ -0,0 +1,40 @@ +# represenst the type of lesson package +module JamRuby + class TestDrivePackageTeacher < ActiveRecord::Base + + @@log = Logging.logger[TestDrivePackageTeacher] + + attr_writer :short_bio_temp + attr_accessible :user_id, :test_drive_package_id, :short_bio, :short_bio_temp, as: :admin + + belongs_to :test_drive_package, class_name: "JamRuby::TestDrivePackage" + belongs_to :user, class_name: "JamRuby::User" + + validates :user, presence: true + validates :test_drive_package, presence: true + + after_save :after_save + + # silly pass through for activeadmin. We pass short_bio set here on to teacher + def after_save + if user && user.teacher + if @another_bio.present? + user.teacher.short_bio = @another_bio + user.teacher.save! + end + end + end + + def short_bio_temp=(short_bio) + self.updated_at = Time.now + self.short_bio = short_bio + @another_bio = short_bio + end + def short_bio_temp + if user && user.teacher + user.teacher.short_bio + end + end + end +end + diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index fd3711ab0..c85388f25 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -1,3 +1,4 @@ +require 'kickbox' include Devise::Models module JamRuby @@ -35,18 +36,22 @@ module JamRuby acts_as_mappable - # after_save :check_lat_lng + after_save :update_teacher_pct attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection # updating_password corresponds to a lost_password - attr_accessor :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json - + attr_accessor :test_drive_packaging, :validate_instruments, :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json, :expecting_gift_card belongs_to :icecast_server_group, class_name: "JamRuby::IcecastServerGroup", inverse_of: :users, foreign_key: 'icecast_server_group_id' + has_many :controlled_sessions, :class_name => "JamRuby::MusicSession", inverse_of: :session_controller, foreign_key: :session_controller_id + # authorizations (for facebook, etc -- omniauth) has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization" + has_many :reviews, :class_name => "JamRuby::Review" + has_one :review_summary, :class_name => "JamRuby::ReviewSummary", as: :target + # calendars (for scheduling NOT in music_session) has_many :calendars, :class_name => "JamRuby::Calendar" @@ -58,12 +63,13 @@ module JamRuby has_many :received_friend_requests, :class_name => "JamRuby::FriendRequest", :foreign_key => 'friend_id' # instruments - has_many :musician_instruments, :class_name => "JamRuby::MusicianInstrument", :foreign_key=> 'player_id' + has_many :musician_instruments, :class_name => "JamRuby::MusicianInstrument", :foreign_key => 'player_id' has_many :instruments, :through => :musician_instruments, :class_name => "JamRuby::Instrument" # bands has_many :band_musicians, :class_name => "JamRuby::BandMusician" has_many :bands, :through => :band_musicians, :class_name => "JamRuby::Band" + belongs_to :teacher, :class_name => "JamRuby::Teacher", foreign_key: :teacher_id # genres has_many :genre_players, as: :player, class_name: "JamRuby::GenrePlayer", dependent: :destroy @@ -94,7 +100,7 @@ module JamRuby has_many :followers, :as => :followable, :class_name => "JamRuby::Follow", :dependent => :destroy # text messages - has_many :text_messages, :class_name => "JamRuby:TextMessage", :foreign_key => "target_user_id" + has_many :text_messages, :class_name => "JamRuby::TextMessage", :foreign_key => "target_user_id" # notifications has_many :notifications, :class_name => "JamRuby::Notification", :foreign_key => "target_user_id" @@ -148,16 +154,31 @@ module JamRuby # events has_many :event_sessions, :class_name => "JamRuby::EventSession" + # gift cards + has_many :gift_cards, :class_name => "JamRuby::GiftCard" + has_many :gift_card_purchases, :class_name => "JamRuby::GiftCardPurchase" + + # affiliate_partner 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" - # jam_tracks + # jam_tracks has_many :jam_track_rights, :class_name => "JamRuby::JamTrackRight", :foreign_key => "user_id" has_many :purchased_jam_tracks, -> { order(:created_at) }, :through => :jam_track_rights, :class_name => "JamRuby::JamTrack", :source => :jam_track + # lessons + has_many :lesson_purchases, :class_name => "JamRuby::LessonPackagePurchase", :foreign_key => "user_id", inverse_of: :user + has_many :student_lesson_bookings, :class_name => "JamRuby::LessonBooking", :foreign_key => "user_id", inverse_of: :user + has_many :teacher_lesson_bookings, :class_name => "JamRuby::LessonBooking", :foreign_key => "teacher_id", inverse_of: :teacher + has_many :teacher_distributions, :class_name => "JamRuby::TeacherDistribution", :foreign_key => "teacher_id", inverse_of: :teacher + has_many :teacher_payments, :class_name => "JamRuby::TeacherPayment", :foreign_key => "teacher_id", inverse_of: :teacher + has_many :test_drive_package_choice_teachers, :class_name => "JamRuby::TestDrivePackageChoiceTeacher", :foreign_key => "teacher_id" + has_many :test_drive_package_choices, :class_name => "JamRuby::TestDrivePackageChoice", :foreign_key => "user_id", inverse_of: :user + belongs_to :desired_package, :class_name => "JamRuby::LessonPackageType", :foreign_key => "lesson_package_type_id", inverse_of: :user_desired_packages # used to hold whether user last wanted test drive 4/2/1 + # Shopping carts has_many :shopping_carts, :class_name => "JamRuby::ShoppingCart" @@ -170,17 +191,31 @@ module JamRuby # This causes the authenticate method to be generated (among other stuff) #has_secure_password - has_many :online_presences, :class_name => "JamRuby::OnlinePresence", :foreign_key=> 'player_id' - has_many :performance_samples, :class_name => "JamRuby::PerformanceSample", :foreign_key=> 'player_id' + has_many :online_presences, :class_name => "JamRuby::OnlinePresence", :foreign_key => 'player_id' + has_many :performance_samples, :class_name => "JamRuby::PerformanceSample", :foreign_key => 'player_id' has_one :musician_search, :class_name => 'JamRuby::MusicianSearch' has_one :band_search, :class_name => 'JamRuby::BandSearch' + belongs_to :teacher, :class_name => 'JamRuby::Teacher', foreign_key: :teacher_id + has_many :jam_track_session, :class_name => "JamRuby::JamTrackSession" + + has_many :taken_lessons, :class_name => "JamRuby::LessonSession", inverse_of: :user, foreign_key: :user_id + has_many :taught_lessons, :class_name => "JamRuby::LessonSession", inverse_of: :teacher, foreign_key: :teacher_id + belongs_to :school, :class_name => "JamRuby::School", inverse_of: :students + has_one :owned_school, :class_name => "JamRuby::School", inverse_of: :user + has_many :test_drive_package_choices, :class_name =>"JamRuby::TestDrivePackageChoice" + has_many :jamblasters_users, class_name: "JamRuby::JamblasterUser" + has_many :jamblasters, class_name: 'JamRuby::Jamblaster', through: :jamblasters_users + + before_save :default_anonymous_names before_save :create_remember_token, :if => :should_validate_password? - before_save :stringify_avatar_info , :if => :updating_avatar + before_save :stringify_avatar_info, :if => :updating_avatar - validates :first_name, presence: true, length: {maximum: 50}, no_profanity: true - validates :last_name, presence: true, length: {maximum: 50}, no_profanity: true + after_save :after_save + + validates :first_name, length: {maximum: 50}, no_profanity: true + validates :last_name, length: {maximum: 50}, no_profanity: true validates :biography, length: {maximum: 4000}, no_profanity: true validates :email, presence: true, format: {with: VALID_EMAIL_REGEX} validates :update_email, presence: true, format: {with: VALID_EMAIL_REGEX}, :if => :updating_email @@ -189,29 +224,35 @@ module JamRuby validates_presence_of :password_confirmation, :if => :should_validate_password? validates_confirmation_of :password, :if => :should_validate_password? - validates :terms_of_service, :acceptance => {:accept => true, :on => :create, :allow_nil => false } + validates :terms_of_service, :acceptance => {:accept => true, :on => :create, :allow_nil => false} validates :reuse_card, :inclusion => {:in => [true, false]} + validates :is_a_student, :inclusion => {:in => [true, false]} + validates :is_a_teacher, :inclusion => {:in => [true, false]} validates :has_redeemable_jamtrack, :inclusion => {:in => [true, false]} + validates :gifted_jamtracks, presence: true, :numericality => {:less_than_or_equal_to => 100} validates :subscribe_email, :inclusion => {:in => [nil, true, false]} validates :musician, :inclusion => {:in => [true, false]} validates :show_whats_next, :inclusion => {:in => [nil, true, false]} + validates :is_a_student, :inclusion => {:in => [true, false]} + validates :is_a_teacher, :inclusion => {:in => [true, false]} #validates :mods, json: true - validates_numericality_of :last_jam_audio_latency, greater_than:MINIMUM_AUDIO_LATENCY, less_than:MAXIMUM_AUDIO_LATENCY, :allow_nil => true - validates :last_jam_updated_reason, :inclusion => {:in => [nil, JAM_REASON_REGISTRATION, JAM_REASON_NETWORK_TEST, JAM_REASON_FTUE, JAM_REASON_JOIN, JAM_REASON_IMPORT, JAM_REASON_LOGIN] } + validates_numericality_of :last_jam_audio_latency, greater_than: MINIMUM_AUDIO_LATENCY, less_than: MAXIMUM_AUDIO_LATENCY, :allow_nil => true + validates :last_jam_updated_reason, :inclusion => {:in => [nil, JAM_REASON_REGISTRATION, JAM_REASON_NETWORK_TEST, JAM_REASON_FTUE, JAM_REASON_JOIN, JAM_REASON_IMPORT, JAM_REASON_LOGIN]} # stored in cents - validates_numericality_of :paid_sessions_hourly_rate, greater_than:0, less_than:200000, :if => :paid_sessions + validates_numericality_of :paid_sessions_hourly_rate, greater_than: 0, less_than: 200000, :if => :paid_sessions # stored in cents - validates_numericality_of :paid_sessions_daily_rate, greater_than:0, less_than:5000000, :if => :paid_sessions + validates_numericality_of :paid_sessions_daily_rate, greater_than: 0, less_than: 5000000, :if => :paid_sessions # custom validators - validate :validate_musician_instruments + validate :validate_musician_instruments, if: :validate_instruments validate :validate_current_password validate :validate_update_email validate :validate_avatar_info validate :email_case_insensitive_uniqueness validate :update_email_case_insensitive_uniqueness, :if => :updating_email validate :validate_mods + validate :presence_gift_card, :if => :expecting_gift_card scope :musicians, -> { where(:musician => true) } scope :fans, -> { where(:musician => false) } @@ -219,8 +260,24 @@ module JamRuby scope :musicians_geocoded, -> { musicians.geocoded_users } scope :email_opt_in, -> { where(:subscribe_email => true) } + def after_save + if school_interest && !school_interest_was + AdminMailer.partner({body: "#{email} signed up via the https://www.jamkazam.com/landing/jamclass/schools page.\n\nFull list is here: https://www.jamkazam.com/admin/admin/school_interests", subject: "#{email} is interested in schools"}).deliver_now + if owned_school.nil? + school = School.new + school.user = self + school.save! + end + end + end + def update_teacher_pct + if teacher + teacher.update_profile_pct + end + end + 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", "first_played_jamtrack_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) @@ -231,6 +288,18 @@ module JamRuby end end + def has_any_free_jamtracks + has_redeemable_jamtrack || gifted_jamtracks > 0 + end + + def free_jamtracks + (has_redeemable_jamtrack ? 1 : 0) + gifted_jamtracks + end + + def show_free_jamtrack? + ShoppingCart.user_has_redeemable_jam_track?(self) + end + def failed_qualification(reason) self.last_failed_certified_gear_at = DateTime.now self.last_failed_certified_gear_reason = reason @@ -253,6 +322,12 @@ module JamRuby end end + def presence_gift_card + if self.gift_cards.length == 0 + errors.add(:gift_card, ValidationMessages::NOT_FOUND) + end + end + def validate_current_password # checks if the user put in their current password (used when changing your email, for instance) errors.add(:current_password, ValidationMessages::NOT_YOUR_PASSWORD) if should_confirm_existing_password? && !valid_password?(self.current_password) @@ -295,15 +370,24 @@ module JamRuby online? end - def name - "#{first_name} #{last_name}" + def anonymous? + first_name == 'Anonymous' && last_name == 'Anonymous' end - def location + def name + if anonymous? + 'Anonymous' + else + "#{first_name} #{last_name}" + end + end + + def location(country = false) loc = self.city.blank? ? '' : self.city loc = loc.blank? ? self.state : "#{loc}, #{self.state}" unless self.state.blank? - #loc = loc.blank? ? self.country : "#{loc}, #{self.country}" unless self.country.blank? - # XXX WHY IS COUNTRY COMMENTED OUT? + if country + loc = loc.blank? ? self.country : "#{loc}, #{self.country}" unless self.country.blank? + end loc end @@ -375,7 +459,7 @@ module JamRuby def age now = Time.now.utc.to_date - self.birth_date.nil? ? "" : now.year - self.birth_date.year - (self.birth_date.to_date.change(:year => now.year) > now ? 1 : 0) + self.birth_date.nil? ? "" : now.year - self.birth_date.year - (self.birth_date.to_date.change(:year => now.year) > now ? 1 : 0) end def session_count @@ -424,6 +508,7 @@ module JamRuby a = read_attribute(:audio_latency) a.nil? ? nil : a.to_i end + # ====== END ARTIFICAL ATTRIBUTES def score_info(destination_user) @@ -495,25 +580,26 @@ module JamRuby # used to exclude currently viewed recording recording_exclusion = "claimed_recordings.id != '#{claimed_recording_id}'" if claimed_recording_id recordings = Recording - .joins(:claimed_recordings) - .where(:owner_id => self.id) - .where("claimed_recordings.user_id = '#{self.id}'") - .where('claimed_recordings.is_public=true') - .where(recording_exclusion) - .order('created_at DESC') - .limit(10) + .joins(:claimed_recordings) + .where(:owner_id => self.id) + .where("claimed_recordings.user_id = '#{self.id}'") + .where('claimed_recordings.is_public=true') + .where(recording_exclusion) + .order('created_at DESC') + .limit(10) # used to exclude currently viewed session - session_exclusion = "music_sessions.id != '#{session_id}'" if session_id + session_exclusion = "music_sessions.id != '#{session_id}'" if session_id msh = MusicSession - .where(:user_id => self.id) - .where(:fan_access => true) - .where(session_exclusion) - .order('created_at DESC') - .limit(10) + .where(:user_id => self.id) + .where(:fan_access => true) + .where(session_exclusion) + .order('created_at DESC') + .limit(10) + results = recordings.concat(msh) - results.sort! {|a,b| b.created_at <=> a.created_at}.first(5) + results = results.sort! { |a, b| b.created_at <=> a.created_at }.first(5) end # returns the # of new notifications @@ -544,6 +630,7 @@ module JamRuby def confirm_email! self.email_confirmed = true + self.email_needs_verification = false end def my_session_settings @@ -575,9 +662,7 @@ module JamRuby def to_s return email unless email.nil? - if !first_name.nil? && !last_name.nil? - return first_name + ' ' + last_name - end + return name unless name.nil? id end @@ -589,12 +674,12 @@ module JamRuby # so that should_validate_password? fires self.updating_password = true - attributes = { :password => new_password, :password_confirmation => new_password_confirmation } + attributes = {:password => new_password, :password_confirmation => new_password_confirmation} # taken liberally from Devise::DatabaseAuthenticatable.update_with_password if valid_password?(old_password) - update_attributes(attributes) + update_attributes(attributes) else self.assign_attributes(attributes) self.valid? @@ -615,14 +700,13 @@ module JamRuby user.change_password(new_password, new_password_confirmation) user.save end - + def change_password(new_password, new_password_confirmation) # FIXME: Should verify that the new password meets certain quality criteria. Really, maybe that should be a # verification step. - self.updating_password = true + self.updating_password = true self.password = new_password self.password_confirmation = new_password_confirmation - UserMailer.password_changed(self).deliver_now end @@ -636,7 +720,6 @@ module JamRuby reset_url = "#{base_uri}/reset_password_token?token=#{user.reset_password_token}&email=#{CGI.escape(email)}" UserMailer.password_reset(user, reset_url).deliver_now - user end @@ -657,11 +740,11 @@ module JamRuby if hide_private recordings = Recording.joins(:musician_recordings) - .where(:musicians_recordings => {:user_id => "#{user_id}"}, :public => true) + .where(:musicians_recordings => {:user_id => "#{user_id}"}, :public => true) else recordings = Recording.joins(:musician_recordings) - .where(:musicians_recordings => {:user_id => "#{user_id}"}) + .where(:musicians_recordings => {:user_id => "#{user_id}"}) end return recordings @@ -669,7 +752,7 @@ module JamRuby def update_genres(gids, genre_type) unless self.new_record? - GenrePlayer.delete_all(["player_id = ? AND player_type = ? AND genre_type = ?", + GenrePlayer.delete_all(["player_id = ? AND player_type = ? AND genre_type = ?", self.id, self.class.name, genre_type]) end @@ -678,7 +761,7 @@ module JamRuby genre_player.player_id = self.id genre_player.player_type = self.class.name genre_player.genre_id = gid - genre_player.genre_type = genre_type + genre_player.genre_type = genre_type self.genre_players << genre_player end end @@ -690,7 +773,7 @@ module JamRuby unless online_presences.nil? online_presences.each do |op| - new_presence = OnlinePresence.create(self, op, false) + new_presence = OnlinePresence.create(self, op, false) self.online_presences << new_presence end end @@ -746,7 +829,7 @@ module JamRuby # this easy_save routine guards against nil sets, but many of these fields can be set to null. # I've started to use it less as I go forward def easy_save(first_name, last_name, email, password, password_confirmation, musician, gender, - birth_date, internet_service_provider, city, state, country, instruments, photo_url, biography = nil) + birth_date, internet_service_provider, city, state, country, instruments, photo_url, biography = nil) # first name unless first_name.nil? @@ -829,7 +912,7 @@ module JamRuby # helper method for creating / updating a User def self.save(id, updater_id, first_name, last_name, email, password, password_confirmation, musician, gender, - birth_date, internet_service_provider, city, state, country, instruments, photo_url, biography) + birth_date, internet_service_provider, city, state, country, instruments, photo_url, biography) if id.nil? user = User.new() else @@ -838,7 +921,7 @@ module JamRuby if user.id != updater_id raise JamPermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR - end + end user.easy_save(first_name, last_name, email, password, password_confirmation, musician, gender, birth_date, internet_service_provider, city, state, country, instruments, photo_url, biography) @@ -922,7 +1005,7 @@ module JamRuby # def create_recording_like(targetRecordingId) # targetRecording = Recording.find(targetRecordingId) - + # like = Like.new # like.likable = targetRecording # like.user = self @@ -937,7 +1020,7 @@ module JamRuby user.email = user.update_email user.update_email_token = nil user.save - begin + begin RecurlyClient.new.update_account(user) rescue Recurly::Error @@log.debug("No recurly account found; continuing") @@ -954,7 +1037,7 @@ module JamRuby end def favorite_count - 0 # FIXME: update this with recording likes count when implemented + 0 # FIXME: update this with recording likes count when implemented end def self.delete_favorite(user_id, recording_id) @@ -988,11 +1071,33 @@ module JamRuby :invitees => invitees } + user.session_settings = session_settings user.save end end + def handle_test_drive_package(package, details) + self.test_drive_packaging = true + choice = TestDrivePackageChoice.new + choice.user = self + choice.test_drive_package = package + details[:teachers].each do |teacher| + teacher_choice = TestDrivePackageChoiceTeacher.new + teacher_choice.teacher = User.find(teacher[:id]) + choice.test_drive_package_choice_teachers << teacher_choice + end + + choice.save! + + choice.test_drive_package_choice_teachers.each do |teacher_choice| + booking = LessonBooking.book_packaged_test_drive(self, teacher_choice.teacher, "Please suggest a time that works for you.", choice) + if booking.errors.any? + raise "unable to create booking in package user:#{self.email}" + end + end + end + # throws ActiveRecord::RecordNotFound if instrument is invalid # throws an email delivery error if unable to connect out to SMTP def self.signup(options) @@ -1017,17 +1122,78 @@ module JamRuby reuse_card = options[:reuse_card] signup_hint = options[:signup_hint] affiliate_partner = options[:affiliate_partner] + gift_card = options[:gift_card] + student = options[:student] + teacher = options[:teacher] + school_invitation_code = options[:school_invitation_code] + school_id = options[:school_id] + school_interest = options[:school_interest] + origin = options[:origin] + test_drive_package_details = options[:test_drive_package] + test_drive_package = TestDrivePackage.find_by_name(test_drive_package_details[:name]) if test_drive_package_details + + school = School.find(school_id) if school_id user = User.new - + user.validate_instruments = true UserManager.active_record_transaction do |user_manager| - user.first_name = first_name - user.last_name = last_name + + if school_invitation_code + school_invitation = SchoolInvitation.find_by_invitation_code(school_invitation_code) + if school_invitation + first_name ||= school_invitation.first_name + last_name ||= school_invitation.last_name + school_invitation.accepted = true + school_invitation.save + end + end + + user.first_name = first_name if first_name.present? + user.last_name = last_name if last_name.present? user.email = email user.subscribe_email = true user.terms_of_service = terms_of_service - user.musician = musician user.reuse_card unless reuse_card.nil? + user.gifted_jamtracks = 0 + user.has_redeemable_jamtrack = true + user.is_a_student = !!student + user.is_a_teacher = !!teacher + user.school_interest = !!school_interest + if user.is_a_student || user.is_a_teacher + musician = true + end + user.musician = !!musician + + if origin + user.origin_utm_source = origin["utm_source"] + user.origin_utm_medium = origin["utm_medium"] + user.origin_utm_campaign = origin["utm_campaign"] + user.origin_referrer = origin["referrer"] + else + user.origin_utm_source = 'organic' + user.origin_utm_medium = 'organic' + user.origin_utm_campaign = nil + user.origin_referrer = nil + end + + + if school_id.present? + if user.is_a_student + user.school_id = school_id + user.affiliate_referral = school.affiliate_partner + elsif user.is_a_teacher + school = School.find_by_id(school_id) + school_name = school ? school.name : 'a music school' + user.teacher = Teacher.build_teacher(user, validate_introduction: true, biography: "Empty biography", school_id: school_id) + user.affiliate_referral = school.affiliate_partner + end + else + if user.is_a_teacher + user.teacher = Teacher.build_teacher(user, validate_introduction: true, biography: "Empty biography") + end + + end + # FIXME: Setting random password for social network logins. This # is because we have validations all over the place on this. @@ -1036,7 +1202,7 @@ module JamRuby # Seth: I think we need a flag in the signature of signup to say 'social_signup=true'. If that flag is set, # then you can do use.updating_password = false and instead set a null password if password.nil? - user.password = user.password_confirmation = SecureRandom.urlsafe_base64 + user.password = user.password_confirmation = SecureRandom.urlsafe_base64 else user.password = password user.password_confirmation = password_confirmation @@ -1132,8 +1298,22 @@ module JamRuby end end + found_gift_card = nil + + # if a gift card value was passed in, then try to find that gift card and apply it to user + if gift_card + user.expecting_gift_card = true + found_gift_card = GiftCard.where(code: gift_card).where(user_id: nil).first + user.gift_cards << found_gift_card if found_gift_card + end + user.save + if found_gift_card + user.reload + ShoppingCart.apply_gifted_jamtracks(user) + end + # 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 && @@ -1148,6 +1328,39 @@ module JamRuby user.errors.add("recaptcha", "verification failed") if recaptcha_failed + unless user.errors.any? + if Rails.application.config.verify_email_enabled + client = Kickbox::Client.new(Rails.application.config.kickbox_api_key) + kickbox = client.kickbox() + response = kickbox.verify(email) + result = response.body["result"] + + user.kickbox_response = response.body.to_json + if result == "deliverable" + user.email_needs_verification = false + elsif result == "undeliverable" + + did_you_mean = response.body["did_you_mean"] + if did_you_mean + user.errors.add(:email, "Did you mean #{did_you_mean}?") + else + user.errors.add(:email, "is not real") + end + elsif result == "risky" || result == "unknown" + if response.body["disposable"] + user.errors.add(:email, "is disposable address") + else + user.email_needs_verification = true + end + end + else + user.email_needs_verification = false + end + end + + + user.save unless user.errors.any? + if user.errors.any? raise ActiveRecord::Rollback else @@ -1163,18 +1376,31 @@ module JamRuby user.save end if affiliate_referral_id.present? - # don't send an signup email if email is already confirmed - if user.email_confirmed - UserMailer.welcome_message(user).deliver_now + + user.handle_test_drive_package(test_drive_package, test_drive_package_details) if test_drive_package + + if user.is_a_student + UserMailer.student_welcome_message(user).deliver_now + elsif user.is_a_teacher + UserMailer.teacher_welcome_message(user).deliver_now + elsif user.school_interest + UserMailer.school_owner_welcome_message(user).deliver_now else + UserMailer.welcome_message(user).deliver_now + end + + if !user.email_confirmed # any errors here should also rollback the transaction; that's OK. If emails aren't going to be delivered, # it's already a really bad situation; make user signup again UserMailer.confirm_email(user, signup_confirm_url.nil? ? nil : (signup_confirm_url + "/" + user.signup_token) ).deliver_now end end end + user.reload if user.id # gift card adding gifted_jamtracks doesn't reflect here until reload user - end # def signup + end + + # def signup # this is intended to be development-mode or test-mode only; VRFS-149 # it creates or updates one user per developer, so that we aren't in the business @@ -1184,7 +1410,7 @@ module JamRuby # because otherwise it's a bit of uncomfortable code # to have sitting around def self.create_dev_user(first_name, last_name, email, password, - city, state, country, instruments, photo_url) + city, state, country, instruments, photo_url) if Environment.mode == "production" # short-circuit out @@ -1192,7 +1418,6 @@ module JamRuby end user = User.find_or_create_by({email:email}) - User.transaction do user.first_name = first_name user.last_name = last_name @@ -1298,15 +1523,15 @@ module JamRuby cropped_large_s3_path = cropped_large_fpfile["key"] self.update_attributes( - :original_fpfile => original_fpfile, - :cropped_fpfile => cropped_fpfile, - :cropped_large_fpfile => cropped_large_fpfile, - :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 => true), - :large_photo_url => S3Util.url(aws_bucket, escape_filename(cropped_large_s3_path), :secure => true) - ) + :original_fpfile => original_fpfile, + :cropped_fpfile => cropped_fpfile, + :cropped_large_fpfile => cropped_large_fpfile, + :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 => true), + :large_photo_url => S3Util.url(aws_bucket, escape_filename(cropped_large_s3_path), :secure => true) + ) end def delete_avatar(aws_bucket) @@ -1339,7 +1564,7 @@ module JamRuby # there are plenty of confirmed users with nil signup_tokens, so we can't look on it raise ActiveRecord::RecordNotFound else - UserManager.active_record_transaction do |user_manager| + User.transaction do # throws ActiveRecord::RecordNotFound if invalid user = User.find_by_signup_token!(signup_token) user.signup_confirm @@ -1397,10 +1622,10 @@ module JamRuby if user_authorization.nil? user_authorization = UserAuthorization.new(provider: 'twitter', - uid: twitter_uid, - token: token, - secret: secret, - user: self) + uid: twitter_uid, + token: token, + secret: secret, + user: self) else user_authorization.uid = twitter_uid user_authorization.token = token @@ -1458,6 +1683,7 @@ module JamRuby def self.after_maxmind_import update_locidispids end + # def check_lat_lng # if (city_changed? || state_changed? || country_changed?) && !lat_changed? && !lng_changed? # update_lat_lng @@ -1525,9 +1751,9 @@ module JamRuby def top_followings @topf ||= User.joins("INNER JOIN follows ON follows.followable_id = users.id AND follows.followable_type = '#{self.class.to_s}'") - .where(['follows.user_id = ?', self.id]) - .order('follows.created_at DESC') - .limit(3) + .where(['follows.user_id = ?', self.id]) + .order('follows.created_at DESC') + .limit(3) end def nearest_musicians @@ -1606,6 +1832,7 @@ module JamRuby !approved_slots.blank? end + # end devise compatibility def self.stats @@ -1628,6 +1855,11 @@ module JamRuby ShoppingCart.where("user_id=?", self).destroy_all end + def destroy_jam_track_shopping_carts + ShoppingCart.destroy_all(anonymous_user_id: @id, cart_type: JamTrack::PRODUCT_TYPE) + end + + def unsubscribe_token self.class.create_access_token(self) end @@ -1674,28 +1906,353 @@ module JamRuby options end - def should_attribute_sale?(shopping_cart) + def should_attribute_sale?(shopping_cart, instance = nil) + + if shopping_cart.is_lesson? && shopping_cart.cart_product.is_test_drive? + # never attribute test drives + return false + end + + if affiliate_referral - referral_info = affiliate_referral.should_attribute_sale?(shopping_cart) + referral_info = affiliate_referral.should_attribute_sale?(shopping_cart, self, instance) else false end + end + def redeem_free_credit + using_free_credit = false + if self.has_redeemable_jamtrack + User.where(id: self.id).update_all(has_redeemable_jamtrack: false) + self.has_redeemable_jamtrack = false + using_free_credit = true + + elsif 0 < self.gifted_jamtracks + User.where(id: self.id).update_all(gifted_jamtracks: self.gifted_jamtracks - 1) + self.gifted_jamtracks = self.gifted_jamtracks - 1 + using_free_credit = true + end + using_free_credit + end + + def has_stored_credit_card? + stored_credit_card + end + + def has_free_lessons? + remaining_free_lessons > 0 + end + + def can_book_free_lesson? + has_free_lessons? && has_stored_credit_card? + end + + def can_buy_test_drive? + lesson_purchases.where('lesson_package_type_id in (?)', LessonPackageType.test_drive_package_ids).where('created_at > ?', APP_CONFIG.test_drive_wait_period_year.years.ago).count == 0 + end + + def lessons_with_teacher(teacher) + taken_lessons.where(teacher_id: teacher.id) + end + + def lessons_with_student(student) + taught_lessons.where(user_id: student.id) + end + + def has_test_drives? + remaining_test_drives > 0 + end + + def has_unprocessed_test_drives? + !unprocessed_test_drive.nil? + end + + def has_requested_test_drive?(teacher = nil) + !requested_test_drive(teacher).nil? + end + + def stripe_auth + user_authorizations.where(provider: "stripe_connect").first + end + + def has_stripe_connect? + auth = stripe_auth + auth && (!auth.token_expiration || auth.token_expiration > Time.now) + end + + def fetch_stripe_customer + Stripe::Customer.retrieve(stripe_customer_id) + end + + # if the user already has a stripe customer, then keep it synced. otherwise create it + def sync_stripe_customer + if self.stripe_customer_id + # we already have a customer for this user; re-use it + customer = fetch_stripe_customer + + if customer.email.nil? || customer.email.downcase != email.downcase + customer.email = email + customer.save + end + else + customer = Stripe::Customer.create( + :description => admin_url, + :source => stripe_token, + :email => email) + end + self.stripe_customer_id = customer.id + User.where(id: id).update_all(stripe_customer_id: customer.id) + + customer + end + + def card_approved(token, zip, booking_id, test_drive_package_choice_id = nil) + + approved_booking = nil + choice = nil + found_uncollectables = nil + User.transaction do + self.stripe_token = token if token + self.stripe_zip_code = zip if zip + customer = sync_stripe_customer + self.stripe_customer_id = customer.id + self.stored_credit_card = true + if self.save + if booking_id + approved_booking = LessonBooking.find_by_id(booking_id) + if approved_booking + approved_booking.card_approved + end + end + + if test_drive_package_choice_id + choice = TestDrivePackageChoice.find(test_drive_package_choice_id) + choice.lesson_bookings.each do|booking| + booking.card_approved + end + end + + if uncollectables.count > 0 + found_uncollectables = uncollectables + uncollectables.update_all(billing_should_retry: true) + else + found_uncollectables = nil + end + end + end + [approved_booking, found_uncollectables, choice] + end + + def update_name(name) + if name.blank? + self.first_name = '' + self.last_name = '' + else + bits = name.split + if bits.length == 1 + self.first_name = '' + self.last_name = bits[0].strip + elsif bits.length == 2 + self.first_name = bits[0].strip + self.last_name = bits[1].strip + else + self.first_name = bits[0].strip + self.last_name = bits[1..-1].join(' ') + end + end + self.save + end + + def payment_update(params) + booking = nil + test_drive = nil + normal = nil + intent = nil + purchase = nil + lesson_package_type = nil + uncollectables = nil + choice = nil + User.transaction do + + if params[:name].present? + if !self.update_name(params[:name]) + return nil + end + end + + booking, uncollectables, choice = card_approved(params[:token], params[:zip], params[:booking_id], params[:test_drive_package_choice_id]) + if params[:test_drive] + self.reload + if booking + lesson_package_type = booking.resolved_test_drive_package + elsif choice + lesson_package_type = choice.lesson_package_type + end + + if lesson_package_type.nil? + lesson_package_type = LessonPackageType.test_drive_4 + end + + + result = Sale.purchase_test_drive(self, lesson_package_type, booking) + test_drive = result[:sale] + purchase = result[:purchase] + + if booking && !purchase.errors.any? + # the booking would not have a lesson_package_purchase associated yet, so let's associate it + booking.lesson_sessions.update_all(lesson_package_purchase_id: purchase.id) + end + elsif params[:normal] + self.reload + end + + end + + {lesson: booking, test_drive: test_drive, purchase: purchase, lesson_package_type: lesson_package_type, uncollectables: uncollectables, package: choice} + end + + def requested_test_drive(teacher = nil) + query = LessonBooking.requested(self).where(lesson_type: LessonBooking::LESSON_TYPE_TEST_DRIVE) + if teacher + query = query.where(teacher_id: teacher.id) + end + query.first + end + + def unprocessed_test_drive + LessonBooking.unprocessed(self).where(lesson_type: LessonBooking::LESSON_TYPE_TEST_DRIVE).first + end + + def unprocessed_normal_lesson + LessonBooking.unprocessed(self).where(lesson_type: LessonBooking::LESSON_TYPE_PAID).first + end + + def most_recent_test_drive_purchase + lesson_purchases.where('lesson_package_type_id in (?)', LessonPackageType.test_drive_package_ids).order('created_at desc').first + end + + def total_test_drives + purchase = most_recent_test_drive_purchase + if purchase + purchase.test_drive_count + else + 0 + end + end + + def test_drive_succeeded(lesson_session) + if self.remaining_test_drives <= 0 + UserMailer.student_test_drive_lesson_done(lesson_session).deliver_now + UserMailer.teacher_lesson_completed(lesson_session).deliver_now + else + UserMailer.student_test_drive_lesson_completed(lesson_session).deliver_now + UserMailer.teacher_lesson_completed(lesson_session).deliver_now + end + end + + def test_drive_declined(lesson_session) + # because we decrement test_drive credits as soon as you book, we need to bring it back now + if lesson_session.lesson_booking.user_decremented + self.remaining_test_drives = self.remaining_test_drives + 1 + self.save(validate: false) + end end - private - def create_remember_token - self.remember_token = SecureRandom.urlsafe_base64 - end - def stringify_avatar_info - # fpfile comes in as a hash, which is a easy-to-use and validate form. However, we store it as a VARCHAR, - # so we need t oconvert it to JSON before storing it (otherwise it gets serialized as a ruby object) - # later, when serving this data out to the REST API, we currently just leave it as a string and make a JSON capable - # client parse it, because it's very rare when it's needed at all - self.original_fpfile = original_fpfile.to_json if !original_fpfile.nil? - self.cropped_fpfile = cropped_fpfile.to_json if !cropped_fpfile.nil? - self.crop_selection = crop_selection.to_json if !crop_selection.nil? + def test_drive_failed(lesson_session) + + if lesson_session.lesson_booking.user_decremented + # because we decrement test_drive credits as soon as you book, we need to bring it back now + self.remaining_test_drives = self.remaining_test_drives + 1 + self.save(validate: false) end + UserMailer.teacher_test_drive_no_bill(lesson_session).deliver_now + UserMailer.student_test_drive_no_bill(lesson_session).deliver_now + end + + def used_test_drives + total_test_drives - remaining_test_drives + end + + def uncollectables(limit = 10) + LessonPaymentCharge.where(user_id:self.id).order(:created_at).where('billing_attempts > 0').where(billed: false).limit(limit) + end + + def has_rated_teacher(teacher) + teacher_rating(teacher).count > 0 + end + + def teacher_rating(teacher) + if teacher.is_a?(JamRuby::User) + teacher = teacher.teacher + end + Review.where(target_id: teacher.id).where(target_type: teacher.class.to_s) + end + + def has_rated_student(student) + student_rating(student).count > 0 + end + + def student_rating(student) + Review.where(target_id: student.id).where(target_type: "JamRuby::User") + end + + def teacher_profile_url + "#{APP_CONFIG.external_root_url}/client#/profile/teacher/#{id}" + end + + def profile_url + "#{APP_CONFIG.external_root_url}/client#/profile/#{id}" + end + + def ratings_url + "#{APP_CONFIG.external_root_url}/client?tile=ratings#/profile/teacher/#{id}" + end + + def student_ratings_url + "#{APP_CONFIG.external_root_url}/client?selected=ratings#/profile/#{id}" + end + + def self.search_url + "#{APP_CONFIG.external_root_url}/client#/jamclass/searchOptions" + end + + def recent_test_drive_teachers + User.select('distinct on (users.id) users.*').joins(taught_lessons: :music_session).where('lesson_sessions.lesson_type = ?', LessonSession::LESSON_TYPE_TEST_DRIVE).where('music_sessions.user_id = ?', id).where('lesson_sessions.created_at > ?', APP_CONFIG.test_drive_wait_period_year.years.ago) + end + + def mark_session_ready + self.ready_for_session_at = Time.now + self.save! + end + + def has_booked_with_student?(student, since_at = nil) + LessonBooking.engaged_bookings(student, self, since_at).count > 0 + end + + def has_booked_test_drive_with_student?(student, since_at = nil) + LessonBooking.engaged_bookings(student, self, since_at).test_drive.count > 0 + end + + private + def create_remember_token + self.remember_token = SecureRandom.urlsafe_base64 + end + + def default_anonymous_names + self.first_name = 'Anonymous' if self.first_name.nil? + self.last_name = 'Anonymous' if self.last_name.nil? + end + + def stringify_avatar_info + # fpfile comes in as a hash, which is a easy-to-use and validate form. However, we store it as a VARCHAR, + # so we need t oconvert it to JSON before storing it (otherwise it gets serialized as a ruby object) + # later, when serving this data out to the REST API, we currently just leave it as a string and make a JSON capable + # client parse it, because it's very rare when it's needed at all + self.original_fpfile = original_fpfile.to_json if !original_fpfile.nil? + self.cropped_fpfile = cropped_fpfile.to_json if !cropped_fpfile.nil? + self.crop_selection = crop_selection.to_json if !crop_selection.nil? + end end end diff --git a/ruby/lib/jam_ruby/models/user_authorization.rb b/ruby/lib/jam_ruby/models/user_authorization.rb index e83bb8db4..9710e7082 100644 --- a/ruby/lib/jam_ruby/models/user_authorization.rb +++ b/ruby/lib/jam_ruby/models/user_authorization.rb @@ -1,7 +1,7 @@ module JamRuby class UserAuthorization < ActiveRecord::Base - attr_accessible :provider, :uid, :token, :token_expiration, :secret, :user + attr_accessible :provider, :uid, :token, :token_expiration, :secret, :user, :refresh_token self.table_name = "user_authorizations" @@ -12,6 +12,40 @@ module JamRuby validates_uniqueness_of :uid, scope: :provider # token, secret, token_expiration can be missing + def is_active? + token_expiration && token_expiration < Time.now + end + + def self.refreshing_google_auth(user) + auth = self.where(:user_id => user.id) + .where(:provider => 'google_login') + .limit(1).first + + # if we have an auth that will expire in less than 10 minutes + if auth && auth.refresh_token && auth.token_expiration < Time.now - 60 * 10 + + begin + oauth_client = OAuth2::Client.new( + Rails.application.config.google_client_id, Rails.application.config.google_secret, + :site => "https://accounts.google.com", + :token_url => "/o/oauth2/token", + :authorize_url => "/o/oauth2/auth") + access_token = OAuth2::AccessToken.from_hash(oauth_client, {:refresh_token => auth.refresh_token}) + access_token = access_token.refresh! + + auth.token = access_token.token + auth.token_expiration = Time.now + access_token.expires_in + auth.save + return auth + rescue Exception => e + # couldn't refresh; probably the user has revoked the app's rights + return nil + end + else + auth + end + end + def self.google_auth(user) self .where(:user_id => user.id) diff --git a/ruby/lib/jam_ruby/models/user_blacklist.rb b/ruby/lib/jam_ruby/models/user_blacklist.rb new file mode 100644 index 000000000..f9285e627 --- /dev/null +++ b/ruby/lib/jam_ruby/models/user_blacklist.rb @@ -0,0 +1,33 @@ +module JamRuby + class UserBlacklist < ActiveRecord::Base + + attr_accessible :user_id, :notes, as: :admin + @@log = Logging.logger[UserBlacklist] + + belongs_to :user, :class_name => "JamRuby::User" + + validates :user, presence: true + + validates_uniqueness_of :user_id + + def self.banned(user) + UserBlacklist.where("user_id = '#{user.id}' AND user_id NOT IN (SELECT white.user_id FROM user_whitelists white WHERE white.user_id = '#{user.id}')").count >= 1 + end + + def self.listed(user) + UserBlacklist.where("user_id= '#{user.id}'").count == 1 + end + + def self.admin_url + APP_CONFIG.admin_root_url + "/admin/user_blacklists/" + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/user_blacklists/" + id + end + + def to_s + user + end + end +end diff --git a/ruby/lib/jam_ruby/models/user_event.rb b/ruby/lib/jam_ruby/models/user_event.rb new file mode 100644 index 000000000..21446c005 --- /dev/null +++ b/ruby/lib/jam_ruby/models/user_event.rb @@ -0,0 +1,8 @@ +module JamRuby + class UserEvent < ActiveRecord::Base + + belongs_to :user, class_name: 'JamRuby::User' + + validates :name, presence: true + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/user_whitelist.rb b/ruby/lib/jam_ruby/models/user_whitelist.rb new file mode 100644 index 000000000..d8f496939 --- /dev/null +++ b/ruby/lib/jam_ruby/models/user_whitelist.rb @@ -0,0 +1,29 @@ +module JamRuby + class UserWhitelist < ActiveRecord::Base + + attr_accessible :user_id, :notes, as: :admin + @@log = Logging.logger[UserWhitelist] + + belongs_to :user, :class_name => "JamRuby::User" + + validates :user, presence:true + + validates_uniqueness_of :user_id + + def self.listed(user) + UserWhitelist.where("user_id= '#{user.id}'").count == 1 + end + + def self.admin_url + APP_CONFIG.admin_root_url + "/admin/user_whitelists/" + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/user_whitelists/" + id + end + + def to_s + user + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/mq_router.rb b/ruby/lib/jam_ruby/mq_router.rb index 8aec53cde..be4ee21c6 100644 --- a/ruby/lib/jam_ruby/mq_router.rb +++ b/ruby/lib/jam_ruby/mq_router.rb @@ -70,6 +70,12 @@ class MQRouter publish_to_client(MessageFactory::ALL_NATIVE_CLIENTS, client_msg) end + + # sends a message to all clients + def publish_to_active_clients(client_msg) + publish_to_client(MessageFactory::ALL_ACTIVE_CLIENTS, client_msg) + end + # sends a message to a session with no checking of permissions (RAW USAGE) # this method deliberately has no database interactivity/active_record objects def publish_to_session(music_session_id, client_ids, client_msg, sender = {:client_id => nil}) @@ -89,11 +95,14 @@ class MQRouter # sends a message to a user with no checking of permissions (RAW USAGE) # this method deliberately has no database interactivity/active_record objects - def publish_to_user(user_id, user_msg) + def publish_to_user(user_id, user_msg, sender = {:client_id => nil}) @@log.error "EM not running in publish_to_user" unless EM.reactor_running? EM.schedule do - @@log.debug "publishing to user:#{user_id} from server" + sender_client_id = sender[:client_id] + + @@log.debug "publishing to user:#{user_id} from server from client:#{sender_client_id}" + # put it on the topic exchange for users self.class.user_exchange.publish(user_msg, :routing_key => "user.#{user_id}") end diff --git a/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb b/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb new file mode 100644 index 000000000..05b7a926d --- /dev/null +++ b/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb @@ -0,0 +1,771 @@ +require 'json' +require 'resque' +require 'resque-retry' +require 'net/http' +require 'digest/md5' + +module JamRuby + class JamTrackMixdownPackager + extend JamRuby::ResqueStats + + include JamRuby::S3ManagerMixin + + TAP_IN_PADDING = 2 + + MAX_PAN = 90 + MIN_PAN = -90 + KNOCK_SECONDS = 0.035 + + attr_accessor :mixdown_package_id, :settings, :mixdown_package, :mixdown, :step + @queue = :jam_track_mixdown_packager + + def log + @log || Logging.logger[JamTrackMixdownPackager] + end + + def self.perform(mixdown_package_id, bitrate=48) + jam_track_builder = JamTrackMixdownPackager.new() + jam_track_builder.mixdown_package_id = mixdown_package_id + jam_track_builder.run + end + + def compute_steps + @step = 0 + number_downloads = @track_settings.length + number_volume_adjustments = (@track_settings.select { |track| should_alter_volume? track }).length + + pitch_shift_steps = @mixdown.will_pitch_shift? ? 1 : 0 + mix_steps = 1 + package_steps = 1 + + number_downloads + number_volume_adjustments + pitch_shift_steps + mix_steps + package_steps + end + + def run + begin + log.info("Mixdown job starting. mixdown_packager_id #{mixdown_package_id}") + begin + @mixdown_package = JamTrackMixdownPackage.find(mixdown_package_id) + + + # bailout check + if @mixdown_package.signed? + log.debug("package is already signed. bailing") + return + end + + @mixdown = @mixdown_package.jam_track_mixdown + @settings = JSON.parse(@mixdown.settings) + + process_jmep + + track_settings + + # compute the step count + total_steps = compute_steps + + # track that it's started ( and avoid db validations ) + signing_started_at = Time.now + last_step_at = Time.now + #JamTrackMixdownPackage.where(:id => @mixdown_package.id).update_all(:signing_started_at => signing_started_at, :should_retry => false, packaging_steps: total_steps, current_packaging_step: 0, last_step_at: last_step_at, :signing => true) + + # because we are skipping 'after_save', we have to keep the model current for the notification. A bit ugly... + + @mixdown_package.current_packaging_step = 0 + @mixdown_package.packaging_steps = total_steps + @mixdown_package.signing_started_at = signing_started_at + @mixdown_package.signing = true + @mixdown_package.should_retry = false + @mixdown_package.last_step_at = last_step_at + @mixdown_package.queued = false + @mixdown_package.save + + SubscriptionMessage.mixdown_signing_job_change(@mixdown_package) + + package + + log.info "Signed mixdown package to #{@mixdown_package[:url]}" + + rescue Exception => e + # record the error in the database + post_error(e) + + #SubscriptionMessage.mixdown_signing_job_change(@mixdown_package) + # and let the job fail, alerting ops too + raise + end + end + end + + def should_alter_volume? track + + # short cut is possible if vol = 1.0 and pan = 0 + vol = track[:vol] + pan = track[:pan] + + vol != 1.0 || pan != 0 + end + + def process_jmep + @start_points = [] + @initial_padding = 0.0 + @tap_in_initial_silence = 0 + + speed = @settings['speed'] || 0 + + @speed_factor = 1.0 + (-speed.to_f / 100.0) + @inverse_speed_factor = 1 - (-speed.to_f / 100) + + log.info("speed factor #{@speed_factor}") + + jmep = @mixdown.jam_track.jmep_json + if jmep + jmep = JSON.parse(jmep) + end + + if jmep.nil? + log.debug("no jmep") + return + end + + events = jmep["Events"] + + return if events.nil? || events.length == 0 + + metronome = nil + events.each do |event| + if event.has_key?("metronome") + metronome = event["metronome"] + break + end + end + + if metronome.nil? || metronome.length == 0 + log.debug("no metronome events for jmep", jmep) + return + end + + @start_points = metronome.select { |x| puts x.inspect; x["action"] == "start" } + + log.debug("found #{@start_points.length} metronome start points") + + start_point = @start_points[0] + + if start_point + start_time = parse_time(start_point["ts"]) + + if start_time < 2.0 + padding = start_time - 2.0 + @initial_padding = padding.abs + @initial_tap_in = start_time + end + end + + if @speed_factor != 1.0 + metronome.length.times do |count| + + # we expect to find metronome start/stop grouped + if count % 2 == 0 + + start = metronome[count] + stop = metronome[count + 1] + + if start["action"] != "start" || stop["action"] != "stop" + # bail out + log.error("found de-coupled metronome events #{start.to_json} | #{stop.to_json}") + next + end + + bpm = start["bpm"].to_f + stop_time = parse_time(stop['ts']) + ticks = stop['ticks'].to_i + + + new_bpm = bpm * @inverse_speed_factor + new_stop_time = stop_time * @speed_factor + new_start_time = new_stop_time - (60.0/new_bpm * ticks) + + log.info("original bpm:#{bpm} start: #{parse_time(start["ts"])} stop: #{stop_time}") + log.info("updated bpm:#{new_bpm} start: #{new_start_time} stop: #{new_stop_time}") + + stop["ts"] = new_stop_time + start["ts"] = new_start_time + start["bpm"] = new_bpm + stop["bpm"] = new_bpm + + @tap_in_initial_silence = (@initial_tap_in + @initial_padding) * @speed_factor + + end + + end + end + + @start_points = metronome.select { |x| puts x.inspect; x["action"] == "start" } + + end + + # format like: "-0:00:02:820" + def parse_time(ts) + + if ts.is_a?(Float) + return ts + end + + time = 0.0 + negative = false + + if ts.start_with?('-') + negative = true + end + + # parse time_format + bits = ts.split(':').reverse + + bit_position = 0 + bits.each do |bit| + if bit_position == 0 + # milliseconds + milliseconds = bit.to_f + time += milliseconds/1000 + elsif bit_position == 1 + # seconds + time += bit.to_f + elsif bit_position == 2 + # minutes + time += 60 * bit.to_f + elsif bit_position == 3 + # hours + # not bothering + end + + bit_position += 1 + end + + if negative + time = 0.0 - time + end + + time + end + + def path_to_resources + File.join(File.dirname(File.expand_path(__FILE__)), '../../../lib/jam_ruby/app/assets/sounds') + end + + def knock_file + if long_sample_rate == 44100 + knock = File.join(path_to_resources, 'knock44.wav') + else + knock = File.join(path_to_resources, 'knock48.wav') + end + knock + end + + def create_silence(tmp_dir, segment_count, duration) + file = File.join(tmp_dir, "#{segment_count}.wav") + + # -c 2 means stereo + cmd("sox -n -r #{long_sample_rate} -c 2 #{file} trim 0.0 #{duration}", "silence") + + file + end + + def create_tapin_track(tmp_dir) + + return nil if @start_points.length == 0 + + segment_count = 0 + + + #initial_silence = @initial_tap_in + @initial_padding + + initial_silence = @tap_in_initial_silence + + #log.info("tapin data: initial_tap_in: #{@initial_tap_in}, initial_padding: #{@initial_padding}, initial_silence: #{initial_silence}") + + time_points = [] + files = [] + if initial_silence > 0 + + files << create_silence(tmp_dir, segment_count, initial_silence) + + time_points << {type: :silence, ts: initial_silence} + segment_count += 1 + end + + + time_cursor = nil + @start_points.each do |start_point| + tap_time = parse_time(start_point["ts"]) + if !time_cursor.nil? + between_silence = tap_time - time_cursor + files << create_silence(tmp_dir, segment_count, between_silence) + time_points << {type: :silence, ts: between_silence} + end + time_cursor = tap_time + bpm = start_point["bpm"].to_f + + tick_silence = 60.0/bpm - KNOCK_SECONDS + + ticks = start_point["ticks"].to_i + + ticks.times do |tick| + files << knock_file + files << create_silence(tmp_dir, segment_count, tick_silence) + time_points << {type: :knock, ts: KNOCK_SECONDS} + time_points << {type: :silence, ts: tick_silence} + time_cursor + 60.0/bpm + segment_count += 1 + end + end + + log.info("time points for tap-in: #{time_points.inspect}") + # do we need to pad with time? not sure + + sequence_cmd = "sox " + files.each do |file| + sequence_cmd << "\"#{file}\" " + end + + count_in = File.join(tmp_dir, "count-in.wav") + sequence_cmd << "\"#{count_in}\"" + + cmd(sequence_cmd, "count_in") + @count_in_file = count_in + count_in + end + + # creates a list of tracks to actually mix + def track_settings + altered_tracks = @settings["tracks"] || [] + + @track_settings = [] + + #void slider2Pan(int i, float *f); + + stems = @mixdown.jam_track.stem_tracks + @track_count = stems.length + + @include_count_in = @settings["count-in"] && @start_points.length > 0 && @mixdown_package.encrypt_type.nil? + + # temp + # @include_count_in = true + + if @include_count_in + @track_count += 1 + end + + stems.each do |stem| + + vol = 1.0 + pan = 0 + match = false + skipped = false + # is this stem in the altered_tracks list? + altered_tracks.each do |alteration| + + if alteration["id"] == stem.id + if alteration["mute"] || alteration["vol"] == 0 + log.debug("leaving out track because muted or 0 volume #{alteration.inspect}") + skipped = true + next + else + vol = alteration["vol"] || vol + pan = alteration["pan"] || pan + end + @track_settings << {stem: stem, vol: vol, pan: pan} + match = true + break + end + end + + # if we didn't deliberately skip this one, and if there was no 'match' (meaning user did not specify), then we leave this in unchanged + if !skipped && !match + @track_settings << {stem: stem, vol: vol, pan: pan} + end + end + + if @include_count_in + @track_settings << {count_in: true, vol: 1.0, pan: 0} + end + + @track_settings + end + + def slider_to_pan(pan) + # transpose MIN_PAN to MAX_PAN to + # 0-1.0 range + #assumes abs(MIN_PAN) == abs(MAX_PAN) + # k = f(i) = (i)/(2*MAX_PAN) + 0.5 + # so f(MIN_PAN) = -0.5 + 0.5 = 0 + + k = ((pan * (1.0))/ (2.0 * MAX_PAN)) + 0.5 + l, r = 0 + + if k == 0 + l = 0.0 + r = 1.0 + else + l = Math.sqrt(k) + r = Math.sqrt(1-k) + end + + [l, r] + end + + def package + + log.info("Settings: #{@settings.to_json}") + + Dir.mktmpdir do |tmp_dir| + + # download all files + @track_settings.each do |track| + + if track[:count_in] + file = create_tapin_track(tmp_dir) + bump_step(@mixdown_package) + else + jam_track_track = track[:stem] + + file = File.join(tmp_dir, jam_track_track.id + '.ogg') + + bump_step(@mixdown_package) + + # download each track needed + s3_manager.download(jam_track_track.url_by_sample_rate(@mixdown_package.sample_rate), file) + end + + track[:file] = file + end + + audio_process tmp_dir + end + end + + def audio_process(tmp_dir) + # use sox remix to apply mute, volume, pan settings + + + # step 1: apply pan and volume per track. mute and vol of 0 has already been handled, by virtue of those tracks not being present in @track_settings + # step 2: mix all tracks into single track, dividing by constant number of jam tracks, which is same as done by client backend + # step 3: apply pitch and speed (if applicable) + # step 4: encrypt with jkz (if applicable) + + apply_vol_and_pan tmp_dir + + create_silence_padding tmp_dir + + mix tmp_dir + + pitch_speed tmp_dir + + final_packaging tmp_dir + end + + # output is :volumed_file in each track in @track_settings + def apply_vol_and_pan(tmp_dir) + @track_settings.each do |track| + + jam_track_track = track[:stem] + count_in = track[:count_in] + file = track[:file] + + unless should_alter_volume? track + track[:volumed_file] = file + else + pan_l, pan_r = slider_to_pan(track[:pan]) + + vol = track[:vol] + + # short + channel_l = pan_l * vol + channel_r = pan_r * vol + + bump_step(@mixdown_package) + + # sox claps.wav claps-remixed.wav remix 1v1.0 2v1.0 + + if count_in + volumed_file = File.join(tmp_dir, 'count-in' + '-volumed.ogg') + else + volumed_file = File.join(tmp_dir, jam_track_track.id + '-volumed.ogg') + end + + cmd("sox \"#{file}\" \"#{volumed_file}\" remix 1v#{channel_r} 2v#{channel_l}", 'vol_pan') + + track[:volumed_file] = volumed_file + end + end + end + + def create_silence_padding(tmp_dir) + if @initial_padding > 0 && @include_count_in + + @padding_file = File.join(tmp_dir, "initial_padding.ogg") + + # -c 2 means stereo + cmd("sox -n -r #{long_sample_rate} -c 2 #{@padding_file} trim 0.0 #{@initial_padding}", "initial_padding") + + @track_settings.each do |track| + + next if track[:count_in] + + input = track[:volumed_file] + output = input[0..-5] + '-padded.ogg' + + padd_cmd = "sox '#{@padding_file}' '#{input}' '#{output}'" + + cmd(padd_cmd, "pad_track_with_silence") + track[:volumed_file] = output + end + end + end + + # output is @mix_file + def mix(tmp_dir) + + bump_step(@mixdown_package) + + @mix_file = File.join(tmp_dir, "mix.ogg") + + + pitch = @settings['pitch'] || 0 + speed = @settings['speed'] || 0 + + + real_count = @track_settings.count + real_count -= 1 if @include_count_in + + # if there is only one track to mix, we need to skip mixing (sox will barf if you try to mix one file), but still divide by number of tracks + if real_count <= 1 + mix_divide = 1.0/@track_count + cmd = "sox -v #{mix_divide} \"#{@track_settings[0][:volumed_file]}\" \"#{@mix_file}\"" + cmd(cmd, 'volume_adjust') + else + # sox -m will divide by number of inputs by default. But we purposefully leave out tracks that are mute/no volume (to save downloading/processing time in this job) + # so we need to tell sox to divide by how many tracks there are as a constant, because this is how the client works today + #sox -m -v 1/n file1 -v 1/n file2 out + cmd = "sox -m" + mix_divide = 1.0/@track_count + @track_settings.each do |track| + + # if pitch/shifted, we lay the tap-in after pitch/speed shift + # next if (pitch != 0 || speed != 0) && track[:count_in] + next if track[:count_in] + + volumed_file = track[:volumed_file] + cmd << " -v #{mix_divide} \"#{volumed_file}\"" + end + + + cmd << " \"#{@mix_file}\"" + cmd(cmd, 'mix_adjust') + end + + + end + + def long_sample_rate + sample_rate = 48000 + if @mixdown_package.sample_rate != 48 + sample_rate = 44100 + end + sample_rate + end + + # output is @speed_mix_file + def pitch_speed tmp_dir + + # # usage + # This app will take an ogg, wav, or mp3 file (for the uploads) as its input and output an ogg file. + # Usage: + # sbsms path-to-input.ogg path-to-output.ogg TimeStrech PitchShift + + # input is @mix_file, created by mix() + # output is @speed_mix_file + + pitch = @settings['pitch'] || 0 + speed = @settings['speed'] || 0 + + # if pitch and speed are 0, we do nothing here + if pitch == 0 && speed == 0 + @speed_mix_file = @mix_file + else + bump_step(@mixdown_package) + + @speed_mix_file = File.join(tmp_dir, "speed_mix_file.ogg") + + # usage: sbsms infile<.wav|.aif|.mp3|.ogg> outfile<.ogg> rate[0.01:100] halfsteps[-48:48] outSampleRateInHz + + sample_rate = long_sample_rate + + # rate comes in as a percent (like 5, -5 for 5%, -5%). We need to change that to 1.05/ + sbsms_speed = speed/100.0 + sbsms_speed = 1.0 + sbsms_speed + + sbsms_pitch = pitch + cmd("sbsms \"#{@mix_file}\" \"#{@speed_mix_file}\" #{sbsms_speed} #{sbsms_pitch} #{sample_rate}", 'speed_pitch_shift') + end + + if @include_count_in + # lay the tap-ins over the recording + layered = File.join(tmp_dir, "layered_speed_mix.ogg") + cmd("sox -m '#{@count_in_file}' '#{@speed_mix_file}' '#{layered}'", "layer_tap_in") + @speed_mix_file = layered + end + end + + def final_packaging tmp_dir + + bump_step(@mixdown_package) + + url = nil + private_key = nil + md5 = nil + length = 0 + output = nil + + if @mixdown_package.encrypt_type + output, private_key = encrypt_jkz tmp_dir + else + # create output file to correct output format + output = convert tmp_dir + end + + # upload output to S3 + s3_url = "#{@mixdown_package.store_dir}/#{@mixdown_package.filename}" + s3_manager.upload(s3_url, output) + + length = File.size(output) + computed_md5 = Digest::MD5.new + File.open(output, 'rb').each { |line| computed_md5.update(line) } + md5 = computed_md5.to_s + + @mixdown_package.finish_sign(s3_url, private_key, length, md5.to_s) + end + + # returns output destination, converting if necessary + def convert(tmp_dir) + # if the file already ends with the desired file type, call it a win + if @speed_mix_file.end_with?(@mixdown_package.file_type) + @speed_mix_file + else + # otherwise we need to convert from lastly created file to correct + output = File.join(tmp_dir, "output.#{@mixdown_package.file_type}") + + cmd("#{APP_CONFIG.normalize_ogg_path} --bitrate 192 \"#{@speed_mix_file}\"", 'normalize') + + if @mixdown_package.file_type == JamTrackMixdownPackage::FILE_TYPE_AAC + cmd("ffmpeg -i \"#{@speed_mix_file}\" -c:a libfdk_aac -b:a 128k \"#{output}\"", 'convert_aac') + elsif @mixdown_package.file_type == JamTrackMixdownPackage::FILE_TYPE_MP3 + cmd("ffmpeg -i \"#{@speed_mix_file}\" -ab 192k \"#{output}\"", 'convert_mp3') + else + raise 'unknown file_type' + end + + + output + end + end + + def encrypt_jkz(tmp_dir) + py_root = APP_CONFIG.jamtracks_dir + step = 0 + + private_key = nil + # we need to make the id of the custom mix be the name of the file (ID.ogg) + custom_mix_name = File.join(tmp_dir, "#{@mixdown.id}.ogg") + FileUtils.mv(@speed_mix_file, custom_mix_name) + jam_file_opts = "" + jam_file_opts << " -i #{Shellwords.escape("#{custom_mix_name}+mixdown")}" + + sku = @mixdown_package.id + title = @mixdown.name + output = File.join(tmp_dir, "#{title.parameterize}.jkz") + py_file = File.join(py_root, "jkcreate.py") + version = @mixdown_package.version + + right = @mixdown.jam_track.right_for_user(@mixdown.user) + + if @mixdown_package.sample_rate == 48 + private_key = right.private_key_48 + else + private_key = right.private_key_44 + end + + unless private_key + @error_reason = 'no_private_key' + @error_detail = 'user needs to generate JamTrack for given sample rate' + raise @error_reason + end + + private_key_file = File.join(tmp_dir, 'skey.pem') + File.open(private_key_file, 'w') { |f| f.write(private_key) } + + log.debug("PRIVATE KEY") + log.debug(private_key) + log.info "Executing python source in #{py_file}, outputting to #{tmp_dir} (#{output})" + + 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)} -t #{Shellwords.escape(title)} -V #{Shellwords.escape(version)}" + + Open3.popen3(cli) do |stdin, stdout, stderr, wait_thr| + pid = wait_thr.pid + exit_status = wait_thr.value + err = stderr.read(1000) + out = stdout.read(1000) + #puts "stdout: #{out}, stderr: #{err}" + raise ArgumentError, "Error calling python script: #{err}" if err.present? + raise ArgumentError, "Error calling python script: #{out}" if out && (out.index("No track files specified") || out.index("Cannot find file")) + + private_key = File.read(private_key_file) + end + return output, private_key + end + + def cmd(cmd, type) + + log.debug("executing #{cmd}") + + output = `#{cmd}` + + result_code = $?.to_i + + if result_code == 0 + output + else + @error_reason = type + "_fail" + @error_detail = "#{cmd}, #{output}" + raise "command `#{cmd}` failed." + end + 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(mixdown_package) + step = @step + last_step_at = Time.now + mixdown_package.current_packaging_step = step + mixdown_package.last_step_at = last_step_at + JamTrackMixdownPackage.where(:id => mixdown_package.id).update_all(last_step_at: last_step_at, current_packaging_step: step) + SubscriptionMessage.mixdown_signing_job_change(mixdown_package) + + @step = step + 1 + end + + # set @error_reason before you raise an exception, and it will be sent back as the error reason + # otherwise, the error_reason will be unhandled-job-exception + def post_error(e) + begin + # if error_reason is null, assume this is an unhandled error + unless @error_reason + @error_reason = "unhandled-job-exception" + @error_detail = e.to_s + end + @mixdown_package.finish_errored(@error_reason, @error_detail) + + rescue Exception => e + log.error "unable to post back to the database the error #{e}" + end + end + end +end diff --git a/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb b/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb index 359bdc514..381bb8b83 100644 --- a/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb +++ b/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb @@ -42,7 +42,7 @@ module JamRuby 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) + 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, queued: false) # 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 @@ -50,6 +50,7 @@ module JamRuby @jam_track_right[signing_state_symbol] = true @jam_track_right.should_retry = false @jam_track_right.last_step_at = Time.now + @jam_track_right.queued = false SubscriptionMessage.jam_track_signing_job_change(@jam_track_right) JamRuby::JamTracksManager.save_jam_track_right_jkz(@jam_track_right, self.bitrate) diff --git a/ruby/lib/jam_ruby/resque/resque_hooks.rb b/ruby/lib/jam_ruby/resque/resque_hooks.rb index 00ff464e1..a21eca215 100644 --- a/ruby/lib/jam_ruby/resque/resque_hooks.rb +++ b/ruby/lib/jam_ruby/resque/resque_hooks.rb @@ -35,6 +35,7 @@ Resque.before_first_fork do end JamRuby::Stats.init(config) + end # https://devcenter.heroku.com/articles/forked-pg-connections Resque.before_fork do diff --git a/ruby/lib/jam_ruby/resque/scheduled/daily_job.rb b/ruby/lib/jam_ruby/resque/scheduled/daily_job.rb index 388516441..485baad04 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/daily_job.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/daily_job.rb @@ -5,12 +5,42 @@ module JamRuby @queue = :scheduled_daily_job @@log = Logging.logger[DailyJob] - class << self - def perform + def self.perform @@log.debug("waking up") + + bounced_emails + calendar_manager = CalendarManager.new calendar_manager.cleanup() + @@log.debug("done") + end + + def self.bounced_emails + if APP_CONFIG.check_bounced_emails + start = GenericState.bounce_check_at + + if start.nil? + start = (Date.today - 1).to_s + end + + end_date = Date.today.to_s + + @@log.info("checking bounced emails from #{start} to #{end_date}") + + bounces = SendgridToolkit::Bounces.new + result = bounces.retrieve({start_date: start, end_date: end_date}) + + result.each do |item| + user = User.find_by_email(item["email"]) + if user + User.where(id: user.id).update_all(subscribe_email: false, bounced: true) + end + end + + singleton = GenericState.singleton + singleton.bounce_check_at = end_date + singleton.save! end end end diff --git a/ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb b/ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb new file mode 100644 index 000000000..d9097d3c6 --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb @@ -0,0 +1,18 @@ +module JamRuby + class HourlyJob + extend Resque::Plugins::JamLonelyJob + + @queue = :scheduled_hourly_job + @@log = Logging.logger[HourlyJob] + + def self.perform + @@log.debug("waking up") + + LessonBooking.hourly_check + LessonSession.hourly_check + TeacherPayment.hourly_check + + @@log.debug("done") + end + end +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 5039fc862..bf0942cd6 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb @@ -24,6 +24,11 @@ module JamRuby def perform # this needs more testing + + # let's make sure jobs don't stay falsely queued for too long. 1 hour seems more than enough + JamTrackRight.where("queued = true AND (NOW() - signing_queued_at > '1 hour'::INTERVAL OR NOW() - updated_at > '1 hour'::INTERVAL)").update_all(queued:false) + JamTrackMixdownPackage.unscoped.where("queued = true AND (NOW() - signing_queued_at > '1 hour'::INTERVAL OR NOW() - updated_at > '1 hour'::INTERVAL)").update_all(queued:false) + return #JamTrackRight.ready_to_clean.each do |jam_track_right| # log.debug("deleting files for jam_track_right #{jam_track_right.id}") diff --git a/ruby/lib/jam_ruby/resque/scheduled/minutely_job.rb b/ruby/lib/jam_ruby/resque/scheduled/minutely_job.rb new file mode 100644 index 000000000..ad9df11a3 --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/minutely_job.rb @@ -0,0 +1,16 @@ +module JamRuby + class MinutelyJob + extend Resque::Plugins::JamLonelyJob + + @queue = :scheduled_minutely_job + @@log = Logging.logger[MinutelyJob] + + def self.perform + @@log.debug("waking up") + + LessonSession.minutely_check + + @@log.debug("done") + 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 6bff4192e..78a40aa6c 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb @@ -31,6 +31,7 @@ module JamRuby Stats.write('users', User.stats) Stats.write('sessions', ActiveMusicSession.stats) Stats.write('jam_track_rights', JamTrackRight.stats) + Stats.write('jam_track_mixdown_packages', JamTrackMixdownPackage.stats) end end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 530431014..2c442e074 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -6,9 +6,9 @@ FactoryGirl.define do specific_instruments nil end - sequence(:email) { |n| "person_#{n}@example.com"} - sequence(:first_name) { |n| "Person" } - sequence(:last_name) { |n| "#{n}" } + sequence(:email) { |n| "person_#{n}@example.com" } + sequence(:first_name) { |n| "Person" } + sequence(:last_name) { |n| "#{n}" } password "foobar" password_confirmation "foobar" email_confirmed true @@ -19,6 +19,11 @@ FactoryGirl.define do terms_of_service true last_jam_audio_latency 5 reuse_card true + has_redeemable_jamtrack true + gifted_jamtracks 0 + remaining_free_lessons 1 + remaining_test_drives 0 + stored_credit_card false #u.association :musician_instrument, factory: :musician_instrument, user: u @@ -42,7 +47,7 @@ FactoryGirl.define do factory :austin_user do first_name 'Austin' - sequence(:last_name) { |n| "#{n}" } + sequence(:last_name) { |n| "#{n}" } state 'TX' city 'Austin' last_jam_locidispid { austin_geoip[:locidispid] } @@ -51,7 +56,7 @@ FactoryGirl.define do factory :dallas_user do first_name 'Dallas' - sequence(:last_name) { |n| "#{n}" } + sequence(:last_name) { |n| "#{n}" } state 'TX' city 'Dallas' last_jam_locidispid { dallas_geoip[:locidispid] } @@ -60,7 +65,7 @@ FactoryGirl.define do factory :houston_user do first_name 'Houston' - sequence(:last_name) { |n| "#{n}" } + sequence(:last_name) { |n| "#{n}" } state 'TX' city 'Houston' last_jam_locidispid { houston_geoip[:locidispid] } @@ -69,7 +74,7 @@ FactoryGirl.define do factory :miami_user do first_name 'Miami' - sequence(:last_name) { |n| "#{n}" } + sequence(:last_name) { |n| "#{n}" } state 'FL' city 'Miami' last_jam_locidispid { miami_geoip[:locidispid] } @@ -78,7 +83,7 @@ FactoryGirl.define do factory :seattle_user do first_name 'Seattle' - sequence(:last_name) { |n| "#{n}" } + sequence(:last_name) { |n| "#{n}" } state 'WA' city 'Seattle' last_jam_locidispid { seattle_geoip[:locidispid] } @@ -90,6 +95,20 @@ FactoryGirl.define do connection = FactoryGirl.create(:connection, :user => user, :music_session => active_music_session) end end + factory :teacher_user do + after(:create) do |user, evaluator| + teacher = FactoryGirl.create(:teacher, user: user, price_per_lesson_60_cents: 3000, price_per_month_60_cents: 3000) + user.is_a_teacher = true + user.save! + end + end + end + + factory :teacher, :class => JamRuby::Teacher do + association :user, factory: :user + price_per_lesson_60_cents 3000 + price_per_month_60_cents 3000 + short_bio 'abc def uueue doc neck' end factory :musician_instrument, :class => JamRuby::MusicianInstrument do @@ -140,6 +159,9 @@ FactoryGirl.define do end factory :music_session, :class => JamRuby::MusicSession do + ignore do + student nil + end sequence(:name) { |n| "Music Session #{n}" } sequence(:description) { |n| "Music Session Description #{n}" } fan_chat true @@ -187,7 +209,7 @@ FactoryGirl.define do client_type 'client' gateway 'gateway1' last_jam_audio_latency { user.last_jam_audio_latency if user } - sequence(:channel_id) { |n| "Channel#{n}"} + sequence(:channel_id) { |n| "Channel#{n}" } association :user, factory: :user scoring_timeout Time.now end @@ -209,7 +231,7 @@ FactoryGirl.define do end factory :band, :class => JamRuby::Band do - sequence(:name) { |n| "Band" } + sequence(:name) { |n| "Band" } biography "My Biography" city "Apex" state "NC" @@ -223,63 +245,73 @@ FactoryGirl.define do description { |n| "Genre #{n}" } end + factory :language, :class => JamRuby::Language do + id { |n| "Language #{n}" } + description { |n| "Language #{n}" } + end + + factory :subject, :class => JamRuby::Subject do + id { |n| "Subject #{n}" } + description { |n| "Subject #{n}" } + end + factory :join_request, :class => JamRuby::JoinRequest do text 'let me in to the session!' end factory :track, :class => JamRuby::Track do sound "mono" - sequence(:client_track_id) { |n| "client_track_id#{n}"} - sequence(:client_resource_id) { |n| "resource_id#{n}"} + sequence(:client_track_id) { |n| "client_track_id#{n}" } + sequence(:client_resource_id) { |n| "resource_id#{n}" } end factory :backing_track, :class => JamRuby::BackingTrack do - sequence(:client_track_id) { |n| "client_track_id#{n}"} + sequence(:client_track_id) { |n| "client_track_id#{n}" } filename 'foo.mp3' end factory :video_source, :class => JamRuby::VideoSource do #client_video_source_id "test_source_id" - sequence(:client_video_source_id) { |n| "client_video_source_id#{n}"} + sequence(:client_video_source_id) { |n| "client_video_source_id#{n}" } end factory :recorded_track, :class => JamRuby::RecordedTrack do instrument JamRuby::Instrument.find('acoustic guitar') sound 'stereo' - sequence(:client_id) { |n| "client_id-#{n}"} - sequence(:track_id) { |n| "track_id-#{n}"} - sequence(:client_track_id) { |n| "client_track_id-#{n}"} + sequence(:client_id) { |n| "client_id-#{n}" } + sequence(:track_id) { |n| "track_id-#{n}" } + sequence(:client_track_id) { |n| "client_track_id-#{n}" } md5 'abc' length 1 fully_uploaded true - association :user, factory: :user - association :recording, factory: :recording + association :user, factory: :user + association :recording, factory: :recording end factory :recorded_backing_track, :class => JamRuby::RecordedBackingTrack do - sequence(:client_id) { |n| "client_id-#{n}"} - sequence(:backing_track_id) { |n| "track_id-#{n}"} - sequence(:client_track_id) { |n| "client_track_id-#{n}"} - sequence(:filename) { |n| "filename-{#n}"} - sequence(:url) { |n| "/recordings/blah/#{n}"} + sequence(:client_id) { |n| "client_id-#{n}" } + sequence(:backing_track_id) { |n| "track_id-#{n}" } + sequence(:client_track_id) { |n| "client_track_id-#{n}" } + sequence(:filename) { |n| "filename-{#n}" } + sequence(:url) { |n| "/recordings/blah/#{n}" } md5 'abc' length 1 fully_uploaded true - association :user, factory: :user - association :recording, factory: :recording + association :user, factory: :user + association :recording, factory: :recording end factory :recorded_video, :class => JamRuby::RecordedVideo do - sequence(:client_video_source_id) { |n| "client_video_source_id-#{n}"} + sequence(:client_video_source_id) { |n| "client_video_source_id-#{n}" } fully_uploaded true length 1 - association :user, factory: :user - association :recording, factory: :recording + association :user, factory: :user + association :recording, factory: :recording end factory :recorded_jam_track_track, :class => JamRuby::RecordedJamTrackTrack do - association :user, factory: :user - association :recording, factory: :recording + association :user, factory: :user + association :recording, factory: :recording association :jam_track_track, factory: :jam_track_track end @@ -289,9 +321,9 @@ FactoryGirl.define do factory :recording, :class => JamRuby::Recording do - association :owner, factory: :user - association :music_session, factory: :active_music_session - association :band, factory: :band + association :owner, factory: :user + association :music_session, factory: :active_music_session + association :band, factory: :band factory :recording_with_track do before(:create) { |recording| @@ -304,8 +336,8 @@ FactoryGirl.define do sequence(:name) { |n| "name-#{n}" } sequence(:description) { |n| "description-#{n}" } is_public true - association :genre, factory: :genre - association :user, factory: :user + association :genre, factory: :genre + association :user, factory: :user before(:create) { |claimed_recording, evaluator| claimed_recording.recording = FactoryGirl.create(:recording_with_track, owner: claimed_recording.user) unless evaluator.recording @@ -328,7 +360,7 @@ FactoryGirl.define do sequence(:mp3_url) { |n| "recordings/mp3/#{n}" } completed true - before(:create) {|mix, evaluator| + before(:create) { |mix, evaluator| if evaluator.autowire user = FactoryGirl.create(:user) mix.recording = FactoryGirl.create(:recording_with_track, owner: user) @@ -351,7 +383,7 @@ FactoryGirl.define do mp3_url nil completed false - before(:create) {|mix, evaluator| + before(:create) { |mix, evaluator| if evaluator.autowire user = FactoryGirl.create(:user) mix.user = user @@ -403,63 +435,63 @@ FactoryGirl.define do end factory :icecast_admin_authentication, :class => JamRuby::IcecastAdminAuthentication do - source_pass Faker::Lorem.characters(10) - admin_user Faker::Lorem.characters(10) - admin_pass Faker::Lorem.characters(10) - relay_user Faker::Lorem.characters(10) - relay_pass Faker::Lorem.characters(10) + source_pass Faker::Lorem.characters(10) + admin_user Faker::Lorem.characters(10) + admin_pass Faker::Lorem.characters(10) + relay_user Faker::Lorem.characters(10) + relay_pass Faker::Lorem.characters(10) end factory :icecast_directory, :class => JamRuby::IcecastDirectory do - yp_url_timeout 15 - yp_url Faker::Lorem.characters(10) + yp_url_timeout 15 + yp_url Faker::Lorem.characters(10) end factory :icecast_master_server_relay, :class => JamRuby::IcecastMasterServerRelay do - master_server Faker::Lorem.characters(10) - master_server_port 8000 + master_server Faker::Lorem.characters(10) + master_server_port 8000 master_update_interval 120 - master_username Faker::Lorem.characters(10) - master_pass Faker::Lorem.characters(10) - relays_on_demand 1 + master_username Faker::Lorem.characters(10) + master_pass Faker::Lorem.characters(10) + relays_on_demand 1 end factory :icecast_path, :class => JamRuby::IcecastPath do - base_dir Faker::Lorem.characters(10) - log_dir Faker::Lorem.characters(10) - pid_file Faker::Lorem.characters(10) - web_root Faker::Lorem.characters(10) - admin_root Faker::Lorem.characters(10) + base_dir Faker::Lorem.characters(10) + log_dir Faker::Lorem.characters(10) + pid_file Faker::Lorem.characters(10) + web_root Faker::Lorem.characters(10) + admin_root Faker::Lorem.characters(10) end factory :icecast_logging, :class => JamRuby::IcecastLogging do - access_log Faker::Lorem.characters(10) - error_log Faker::Lorem.characters(10) - log_level 3 - log_archive nil - log_size 10000 + access_log Faker::Lorem.characters(10) + error_log Faker::Lorem.characters(10) + log_level 3 + log_archive nil + log_size 10000 end factory :icecast_security, :class => JamRuby::IcecastSecurity do - chroot 0 + chroot 0 end factory :icecast_mount, :class => JamRuby::IcecastMount do - sequence(:name) { |n| "/mount_#{n}" } - source_username Faker::Lorem.characters(10) - source_pass Faker::Lorem.characters(10) - max_listeners 100 + sequence(:name) { |n| "/mount_#{n}" } + source_username Faker::Lorem.characters(10) + source_pass Faker::Lorem.characters(10) + max_listeners 100 max_listener_duration 3600 - fallback_mount Faker::Lorem.characters(10) - fallback_override 1 - fallback_when_full 1 - is_public -1 - stream_name Faker::Lorem.characters(10) - stream_description Faker::Lorem.characters(10) - stream_url Faker::Lorem.characters(10) - genre Faker::Lorem.characters(10) - hidden 0 - association :server, factory: :icecast_server_with_overrides + fallback_mount Faker::Lorem.characters(10) + fallback_override 1 + fallback_when_full 1 + is_public -1 + stream_name Faker::Lorem.characters(10) + stream_description Faker::Lorem.characters(10) + stream_url Faker::Lorem.characters(10) + genre Faker::Lorem.characters(10) + hidden 0 + association :server, factory: :icecast_server_with_overrides factory :icecast_mount_with_auth do association :authentication, :factory => :icecast_user_authentication @@ -477,39 +509,39 @@ FactoryGirl.define do factory :icecast_source_change, :class => JamRuby::IcecastSourceChange do source_direction true success true - sequence(:client_id) { |n| "client_id#{n}" } + sequence(:client_id) { |n| "client_id#{n}" } change_type JamRuby::IcecastSourceChange::CHANGE_TYPE_CLIENT - association :user, :factory => :user - association :mount, :factory => :iceast_mount_with_music_session + association :user, :factory => :user + association :mount, :factory => :iceast_mount_with_music_session end factory :icecast_listen_socket, :class => JamRuby::IcecastListenSocket do - port 8000 + port 8000 end factory :icecast_relay, :class => JamRuby::IcecastRelay do - port 8000 - mount Faker::Lorem.characters(10) - server Faker::Lorem.characters(10) - on_demand 1 + port 8000 + mount Faker::Lorem.characters(10) + server Faker::Lorem.characters(10) + on_demand 1 end factory :icecast_user_authentication, :class => JamRuby::IcecastUserAuthentication do authentication_type 'url' - unused_username Faker::Lorem.characters(10) - unused_pass Faker::Lorem.characters(10) - mount_add Faker::Lorem.characters(10) - mount_remove Faker::Lorem.characters(10) - listener_add Faker::Lorem.characters(10) - listener_remove Faker::Lorem.characters(10) - auth_header 'icecast-auth-user: 1' - timelimit_header 'icecast-auth-timelimit:' + unused_username Faker::Lorem.characters(10) + unused_pass Faker::Lorem.characters(10) + mount_add Faker::Lorem.characters(10) + mount_remove Faker::Lorem.characters(10) + listener_add Faker::Lorem.characters(10) + listener_remove Faker::Lorem.characters(10) + auth_header 'icecast-auth-user: 1' + timelimit_header 'icecast-auth-timelimit:' end factory :icecast_server, :class => JamRuby::IcecastServer do - sequence(:hostname) { |n| "hostname-#{n}"} - sequence(:server_id) { |n| "test-server-#{n}"} + sequence(:hostname) { |n| "hostname-#{n}" } + sequence(:server_id) { |n| "test-server-#{n}" } factory :icecast_server_minimal do association :template, :factory => :icecast_template_minimal @@ -530,27 +562,27 @@ FactoryGirl.define do end factory :icecast_mount_template, :class => JamRuby::IcecastMountTemplate do - sequence(:name) { |n| "name-#{n}"} - source_username Faker::Lorem.characters(10) - source_pass Faker::Lorem.characters(10) - max_listeners 100 + sequence(:name) { |n| "name-#{n}" } + source_username Faker::Lorem.characters(10) + source_pass Faker::Lorem.characters(10) + max_listeners 100 max_listener_duration 3600 - fallback_mount Faker::Lorem.characters(10) - fallback_override 1 - fallback_when_full 1 - is_public -1 - stream_name Faker::Lorem.characters(10) - stream_description Faker::Lorem.characters(10) - stream_url Faker::Lorem.characters(10) - genre Faker::Lorem.characters(10) - hidden 0 - association :authentication, :factory => :icecast_user_authentication + fallback_mount Faker::Lorem.characters(10) + fallback_override 1 + fallback_when_full 1 + is_public -1 + stream_name Faker::Lorem.characters(10) + stream_description Faker::Lorem.characters(10) + stream_url Faker::Lorem.characters(10) + genre Faker::Lorem.characters(10) + hidden 0 + association :authentication, :factory => :icecast_user_authentication end factory :icecast_template, :class => JamRuby::IcecastTemplate do - sequence(:name) { |n| "name-#{n}"} - sequence(:location) { |n| "location-#{n}"} + sequence(:name) { |n| "name-#{n}" } + sequence(:location) { |n| "location-#{n}" } factory :icecast_template_minimal do association :limit, :factory => :icecast_limit @@ -566,25 +598,25 @@ FactoryGirl.define do end factory :facebook_signup, :class => JamRuby::FacebookSignup do - sequence(:lookup_id) { |n| "lookup-#{n}"} - sequence(:first_name) { |n| "first-#{n}"} - sequence(:last_name) { |n| "last-#{n}"} - gender 'M' - sequence(:email) { |n| "jammin-#{n}@jamkazam.com"} - sequence(:uid) { |n| "uid-#{n}"} - sequence(:token) { |n| "token-#{n}"} - token_expires_at Time.now + sequence(:lookup_id) { |n| "lookup-#{n}" } + sequence(:first_name) { |n| "first-#{n}" } + sequence(:last_name) { |n| "last-#{n}" } + gender 'M' + sequence(:email) { |n| "jammin-#{n}@jamkazam.com" } + sequence(:uid) { |n| "uid-#{n}" } + sequence(:token) { |n| "token-#{n}" } + token_expires_at Time.now end factory :recording_comment, :class => JamRuby::RecordingComment do sequence(:comment) { |n| "comment-#{n}" } - association :recording, factory: :recording - association :user, factory: :recording + association :recording, factory: :recording + association :user, factory: :recording end factory :playable_play, :class => JamRuby::PlayablePlay do - association :user, factory: :user + association :user, factory: :user end factory :recording_like, :class => JamRuby::RecordingLiker do @@ -596,9 +628,9 @@ FactoryGirl.define do end factory :event, :class => JamRuby::Event do - sequence(:slug) { |n| "slug-#{n}" } - title 'event title' - description 'event description' + sequence(:slug) { |n| "slug-#{n}" } + title 'event title' + description 'event description' end factory :event_session, :class => JamRuby::EventSession do @@ -645,7 +677,7 @@ FactoryGirl.define do after(:create) do |rsvp_slot, evaluator| rsvp_request = FactoryGirl.create(:rsvp_request, user: evaluator.user) - rsvp_request_rsvp_slot = FactoryGirl.create(:rsvp_request_rsvp_slot, chosen:true, rsvp_request: rsvp_request, rsvp_slot:rsvp_slot) + rsvp_request_rsvp_slot = FactoryGirl.create(:rsvp_request_rsvp_slot, chosen: true, rsvp_request: rsvp_request, rsvp_slot: rsvp_slot) end end end @@ -663,10 +695,10 @@ FactoryGirl.define do chosen nil end - after(:create) { |rsvp_request, evaluator | + after(:create) { |rsvp_request, evaluator| evaluator.number.times do |i| slot = FactoryGirl.create(:rsvp_slot, music_session: evaluator.music_session, instrument: Instrument.order(:id).limit(1).offset(i).first, proficiency_level: 1) - FactoryGirl.create(:rsvp_request_rsvp_slot, chosen: evaluator.chosen, rsvp_request: rsvp_request, rsvp_slot:slot) + FactoryGirl.create(:rsvp_request_rsvp_slot, chosen: evaluator.chosen, rsvp_request: rsvp_request, rsvp_slot: slot) end } end @@ -678,9 +710,9 @@ FactoryGirl.define do chosen nil end - after(:create) { |rsvp_request, evaluator | + after(:create) { |rsvp_request, evaluator| evaluator.slots.each do |slot| - FactoryGirl.create(:rsvp_request_rsvp_slot, chosen:evaluator.chosen, rsvp_request: rsvp_request, rsvp_slot:slot) + FactoryGirl.create(:rsvp_request_rsvp_slot, chosen: evaluator.chosen, rsvp_request: rsvp_request, rsvp_slot: slot) end } end @@ -693,7 +725,7 @@ FactoryGirl.define do factory :latency_tester, :class => JamRuby::LatencyTester do transient do connection nil - make_connection true + make_connection true end sequence(:client_id) { |n| "LatencyTesterClientId-#{n}" } @@ -723,26 +755,43 @@ FactoryGirl.define do sequence(:phone) { |n| "phone-#{n}" } end + factory :jam_track_mixdown, :class => JamRuby::JamTrackMixdown do + association :user, factory: :user + association :jam_track, factory: :jam_track + sequence(:name) { |n| "mixdown-#{n}" } + settings '{"speed":5}' + end + + factory :jam_track_mixdown_package, :class => JamRuby::JamTrackMixdownPackage do + file_type JamRuby::JamTrackMixdownPackage::FILE_TYPE_OGG + sample_rate 48 + signing false + signed false + + association :jam_track_mixdown, factory: :jam_track_mixdown + end + + factory :jam_track, :class => JamRuby::JamTrack do sequence(:name) { |n| "jam-track-#{n}" } sequence(:description) { |n| "description-#{n}" } sequence(:slug) { |n| "slug-#{n}" } - time_signature '4/4' - status 'Production' - recording_type 'Cover' + time_signature '4/4' + status 'Production' + recording_type 'Cover' sequence(:original_artist) { |n| "original-artist-#{n}" } sequence(:songwriter) { |n| "songwriter-#{n}" } sequence(:publisher) { |n| "publisher-#{n}" } - sales_region 'United States' - price 1.99 - reproduction_royalty true + sales_region 'United States' + price 1.99 + reproduction_royalty true public_performance_royalty true reproduction_royalty_amount 0.999 licensor_royalty_amount 0.999 - sequence(:plan_code) { |n| "jamtrack-#{n}" } + sequence(:plan_code) { |n| "jamtrack-#{n}" } - genres [JamRuby::Genre.first] - association :licensor, factory: :jam_track_licensor + genres [JamRuby::Genre.first] + association :licensor, factory: :jam_track_licensor factory :jam_track_with_tracks do after(:create) do |jam_track, evaluator| @@ -752,32 +801,40 @@ FactoryGirl.define do end factory :jam_track_track, :class => JamRuby::JamTrackTrack do - position 1 - part 'lead guitar' - track_type 'Track' - instrument JamRuby::Instrument.find('electric guitar') - association :jam_track, factory: :jam_track + position 1 + part 'lead guitar' + track_type 'Track' + instrument JamRuby::Instrument.find('electric guitar') + association :jam_track, factory: :jam_track end factory :jam_track_right, :class => JamRuby::JamTrackRight do - association :jam_track, factory: :jam_track - association :user, factory: :user + association :jam_track, factory: :jam_track + association :user, factory: :user signing_44 false signing_48 false end + + factory :download_tracker, :class => JamRuby::DownloadTracker do + remote_ip '1.1.1.1' + paid false + association :user, factory: :user + association :jam_track, factory: :jam_track + end + factory :sale, :class => JamRuby::Sale do order_total 0 - association :user, factory:user + association :user, factory: user end factory :recurly_transaction_web_hook, :class => JamRuby::RecurlyTransactionWebHook do transaction_type JamRuby::RecurlyTransactionWebHook::SUCCESSFUL_PAYMENT - sequence(:recurly_transaction_id ) { |n| "recurly-transaction-id-#{n}" } - sequence(:subscription_id ) { |n| "subscription-id-#{n}" } - sequence(:invoice_id ) { |n| "invoice-id-#{n}" } - sequence(:invoice_number ) { |n| 1000 + n } + sequence(:recurly_transaction_id) { |n| "recurly-transaction-id-#{n}" } + sequence(:subscription_id) { |n| "subscription-id-#{n}" } + sequence(:invoice_id) { |n| "invoice-id-#{n}" } + sequence(:invoice_number) { |n| 1000 + n } invoice_number_prefix nil action 'purchase' status 'success' @@ -827,4 +884,229 @@ FactoryGirl.define do legalese Faker::Lorem.paragraphs(6).join("\n\n") end + factory :gift_card, class: 'JamRuby::GiftCard' do + sequence(:code) { n.to_s } + card_type JamRuby::GiftCardType::JAM_TRACKS_5 + end + + factory :gift_card_type, class: 'JamRuby::GiftCardType' do + card_type JamRuby::GiftCardType::JAM_TRACKS_5 + end + + factory :gift_card_purchase, class: 'JamRuby::GiftCardPurchase' do + + association :user, factory: :user + end + + factory :jamblaster, class: 'JamRuby::Jamblaster' do + + association :user, factory: :user + + sequence(:serial_no) { |n| "serial_no#{n}" } + sequence(:client_id) { |n| "client_id#{n}" } + end + + factory :jamblaster_pairing_request, class: 'JamRuby::JamblasterPairingRequest' do + + association :user, factory: :user + association :jamblaster, factory: :jamblaster + + sequence(:jamblaster_client_id) { |n| "jamblaster_client_id#{n}" } + sequence(:sibling_key) { |n| "sibling_key#{n}" } + end + + factory :school, class: 'JamRuby::School' do + association :user, factory: :user + sequence(:name) { |n| "Dat Music School" } + enabled true + scheduling_communication 'teacher' + end + + factory :school_invitation, class: 'JamRuby::SchoolInvitation' do + association :school, factory: :school + note "hey come in in" + as_teacher true + sequence(:email) { |n| "school_person#{n}@example.com" } + sequence(:first_name) { |n| "FirstName" } + sequence(:last_name) { |n| "LastName" } + accepted false + end + + factory :lesson_booking_slot, class: 'JamRuby::LessonBookingSlot' do + factory :lesson_booking_slot_single do + slot_type 'single' + preferred_day { Date.today + 3 } + day_of_week nil + hour 12 + minute 30 + timezone 'UTC' + end + + factory :lesson_booking_slot_recurring do + slot_type 'recurring' + preferred_day nil + day_of_week 0 + hour 12 + minute 30 + timezone 'UTC' + end + end + + factory :lesson_booking, class: 'JamRuby::LessonBooking' do + association :user, factory: :user + association :teacher, factory: :teacher_user + card_presumed_ok false + sent_notices false + recurring false + lesson_length 30 + lesson_type JamRuby::LessonBooking::LESSON_TYPE_FREE + payment_style JamRuby::LessonBooking::PAYMENT_STYLE_ELSEWHERE + description "Oh my goodness!" + status JamRuby::LessonBooking::STATUS_REQUESTED + before(:create) do |lesson_booking, evaluator| + lesson_booking.lesson_booking_slots = [FactoryGirl.build(:lesson_booking_slot_single, lesson_booking: lesson_booking), + FactoryGirl.build(:lesson_booking_slot_single, lesson_booking: lesson_booking)] + end + #lesson_booking_slots [FactoryGirl.build(:lesson_booking_slot_single), FactoryGirl.build(:lesson_booking_slot_single)] + end + + factory :lesson_package_purchase, class: "JamRuby::LessonPackagePurchase" do + lesson_package_type { JamRuby::LessonPackageType.single } + association :user, factory: :user + association :teacher, factory: :teacher_user + price 30.00 + + factory :test_drive_purchase do + lesson_package_type { JamRuby::LessonPackageType.test_drive_4 } + association :lesson_booking, factory: :lesson_booking + price 49.99 + end + end + + factory :lesson_session, class: 'JamRuby::LessonSession' do + + ignore do + student nil + end + + music_session { FactoryGirl.create(:music_session, creator: student) } + lesson_booking { FactoryGirl.create(:lesson_booking, user: student, teacher: teacher) } + association :teacher, factory: :teacher_user + lesson_type JamRuby::LessonSession::LESSON_TYPE_SINGLE + duration 30 + booked_price 49.99 + status JamRuby::LessonSession::STATUS_REQUESTED + #teacher_complete true + #student_complete true + end + + factory :charge, class: 'JamRuby::Charge' do + type 'JamRuby::Charge' + amount_in_cents 1000 + end + + factory :teacher_payment_charge, parent: :charge, class: 'JamRuby::TeacherPaymentCharge' do + type 'JamRuby::TeacherPaymentCharge' + association :user, factory: :user + end + + + factory :lesson_payment_charge, parent: :charge, class: 'JamRuby::LessonPaymentCharge' do + type 'JamRuby::LessonPaymentCharge' + association :user, factory: :user + end + + + factory :teacher_payment, class: 'JamRuby::TeacherPayment' do + association :teacher, factory: :teacher_user + association :teacher_payment_charge, factory: :teacher_payment_charge + amount_in_cents 1000 + fee_in_cents 0 + end + + # you gotta pass either lesson_session or lesson_package_purchase for this to make sense + factory :teacher_distribution, class: 'JamRuby::TeacherDistribution' do + association :teacher, factory: :teacher_user + association :teacher_payment, factory: :teacher_payment + ready false + amount_in_cents 1000 + end + + + factory :ip_blacklist, class: "JamRuby::IpBlacklist" do + remote_ip '1.1.1.1' + end + + factory :ip_whitelist, class: "JamRuby::IpWhitelist" do + remote_ip '1.1.1.1' + end + + factory :user_blacklist, class: "JamRuby::UserBlacklist" do + association :user, factory: :user + end + + factory :user_whitelist, class: "JamRuby::UserWhitelist" do + association :user, factory: :user + end + + factory :email_blacklist, class: "JamRuby::EmailBlacklist" do + sequence(:email) { |n| "person_#{n}@example.com" } + end + + factory :music_notation, class: "JamRuby::MusicNotation" do + attachment_type { JamRuby::MusicNotation::TYPE_NOTATION } + association :user, factory: :user + file_url 'abc' + size 100 + file_name 'some_file.jpg' + end + + factory :test_drive_package, class: "JamRuby::TestDrivePackage" do + + sequence(:name) { |n| "package-#{n}" } + + trait :one_pack do + package_type 1 + after(:create) do |package, evaluator| + 1.times.each do + FactoryGirl.create(:test_drive_package_teachers, test_drive_package: package) + end + end + end + + trait :two_pack do + package_type 2 + after(:create) do |package, evaluator| + 2.times.each do + FactoryGirl.create(:test_drive_package_teachers, test_drive_package: package) + end + end + end + + trait :four_pack do + package_type 4 + after(:create) do |package, evaluator| + 4.times.each do + FactoryGirl.create(:test_drive_package_teachers, test_drive_package: package) + end + end + end + + end + + factory :test_drive_package_teachers, class: "JamRuby::TestDrivePackageTeacher" do + association :user, factory: :teacher_user + association :test_drive_package, factory: [:test_drive_package, :four_pack] + end + + factory :test_drive_package_choice, class: "JamRuby::TestDrivePackageChoice" do + association :user, factory: :user + association :test_drive_package, factory: [:test_drive_package, :four_pack] + end + + factory :test_drive_package_choice_teacher, class: "JamRuby::TestDrivePackageChoiceTeacher" do + association :teacher, factory: :teacher_user + association :test_drive_package_choice, factory: :test_drive_package_choice + end end + diff --git a/ruby/spec/jam_ruby/connection_manager_spec.rb b/ruby/spec/jam_ruby/connection_manager_spec.rb index 8d265188e..86333e194 100644 --- a/ruby/spec/jam_ruby/connection_manager_spec.rb +++ b/ruby/spec/jam_ruby/connection_manager_spec.rb @@ -50,8 +50,8 @@ describe ConnectionManager, no_transaction: true do user.save! user = nil - @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY) - expect { @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY) }.to raise_error(PG::Error) + @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY, false) + expect { @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY, false) }.to raise_error(PG::Error) end it "create connection then delete it" do @@ -60,7 +60,7 @@ describe ConnectionManager, no_transaction: true do #user_id = create_user("test", "user2", "user2@jamkazam.com") user = FactoryGirl.create(:user) - count = @connman.create_connection(user.id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY) + count = @connman.create_connection(user.id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY, false) count.should == 1 @@ -90,7 +90,7 @@ describe ConnectionManager, no_transaction: true do #user_id = create_user("test", "user2", "user2@jamkazam.com") user = FactoryGirl.create(:user) - count = @connman.create_connection(user.id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY) + count = @connman.create_connection(user.id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY, false) count.should == 1 @@ -130,7 +130,7 @@ describe ConnectionManager, no_transaction: true do #user_id = create_user("test", "user2", "user2@jamkazam.com") user = FactoryGirl.create(:user) - count = @connman.create_connection(user.id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, false, GATEWAY) + count = @connman.create_connection(user.id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, false, GATEWAY, false) count.should == 1 @@ -261,7 +261,7 @@ describe ConnectionManager, no_transaction: true do it "flag stale connection" do client_id = "client_id8" user_id = create_user("test", "user8", "user8@jamkazam.com") - @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY) + @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY, false) num = JamRuby::Connection.count(:conditions => ['aasm_state = ?','connected']) num.should == 1 @@ -302,7 +302,7 @@ describe ConnectionManager, no_transaction: true do it "expires stale connection" do client_id = "client_id8" user_id = create_user("test", "user8", "user8@jamkazam.com") - @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY) + @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY, false) conn = Connection.find_by_client_id(client_id) set_updated_at(conn, Time.now - STALE_BUT_NOT_EXPIRED) @@ -328,7 +328,7 @@ describe ConnectionManager, no_transaction: true do music_session_id = music_session.id user = User.find(user_id) - @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY) + @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY, false) connection = @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) connection.errors.any?.should be_false @@ -345,6 +345,8 @@ describe ConnectionManager, no_transaction: true do assert_session_exists(music_session_id, false) end + + it "join_music_session fails if no connection" do client_id = "client_id10" @@ -354,7 +356,7 @@ describe ConnectionManager, no_transaction: true do user = User.find(user_id) - expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) }.to raise_error(ActiveRecord::RecordNotFound) + expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) }.to raise_error(JamRuby::JamRecordNotFound) end @@ -364,8 +366,8 @@ describe ConnectionManager, no_transaction: true do client_id2 = "client_id10.12" user_id = create_user("test", "user10.11", "user10.11@jamkazam.com", :musician => true) user_id2 = create_user("test", "user10.12", "user10.12@jamkazam.com", :musician => false) - @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY) - @connman.create_connection(user_id2, client_id2, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY) + @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY, false) + @connman.create_connection(user_id2, client_id2, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY, false) music_session = FactoryGirl.create(:active_music_session, user_id: user_id) music_session_id = music_session.id @@ -384,7 +386,7 @@ describe ConnectionManager, no_transaction: true do client_id = "client_id10.2" user_id = create_user("test", "user10.2", "user10.2@jamkazam.com") - @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY) + @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY, false) music_session = FactoryGirl.create(:active_music_session, user_id: user_id) user = User.find(user_id) @@ -400,8 +402,8 @@ describe ConnectionManager, no_transaction: true do fan_client_id = "client_id10.4" musician_id = create_user("test", "user10.3", "user10.3@jamkazam.com") fan_id = create_user("test", "user10.4", "user10.4@jamkazam.com", :musician => false) - @connman.create_connection(musician_id, musician_client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY) - @connman.create_connection(fan_id, fan_client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY) + @connman.create_connection(musician_id, musician_client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY, false) + @connman.create_connection(fan_id, fan_client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY, false) music_session = FactoryGirl.create(:active_music_session, :fan_access => false, user_id: musician_id) music_session_id = music_session.id @@ -425,9 +427,9 @@ describe ConnectionManager, no_transaction: true do music_session_id = music_session.id user = User.find(user_id2) - @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY) + @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY, false) # specify real user id, but not associated with this session - expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) } .to raise_error(ActiveRecord::RecordNotFound) + expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) } .to raise_error(JamRuby::JamPermissionError) end it "join_music_session fails if no music_session" do @@ -437,7 +439,7 @@ describe ConnectionManager, no_transaction: true do user = User.find(user_id) music_session = ActiveMusicSession.new - @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY) + @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY, false) connection = @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) connection.errors.size.should == 1 connection.errors.get(:music_session).should == [ValidationMessages::MUSIC_SESSION_MUST_BE_SPECIFIED] @@ -451,9 +453,9 @@ describe ConnectionManager, no_transaction: true do music_session_id = music_session.id user = User.find(user_id2) - @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY) + @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY, false) # specify real user id, but not associated with this session - expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) } .to raise_error(ActiveRecord::RecordNotFound) + expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) } .to raise_error(JamRuby::JamPermissionError) end @@ -465,7 +467,7 @@ describe ConnectionManager, no_transaction: true do user = User.find(user_id) dummy_music_session = ActiveMusicSession.new - @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY) + @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY, false) expect { @connman.leave_music_session(user, Connection.find_by_client_id(client_id), dummy_music_session) }.to raise_error(JamRuby::StateError) end @@ -480,7 +482,7 @@ describe ConnectionManager, no_transaction: true do dummy_music_session = ActiveMusicSession.new - @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY) + @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY, false) @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) expect { @connman.leave_music_session(user, Connection.find_by_client_id(client_id), dummy_music_session) }.to raise_error(JamRuby::StateError) end @@ -493,7 +495,7 @@ describe ConnectionManager, no_transaction: true do music_session_id = music_session.id user = User.find(user_id) - @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY) + @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY, false) @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) assert_session_exists(music_session_id, true) @@ -536,7 +538,7 @@ describe ConnectionManager, no_transaction: true do user = User.find(user_id) client_id1 = Faker::Number.number(20) - @connman.create_connection(user_id, client_id1, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY) + @connman.create_connection(user_id, client_id1, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME, REACHABLE, GATEWAY, false) music_session1 = FactoryGirl.create(:active_music_session, :user_id => user_id) connection1 = @connman.join_music_session(user, client_id1, music_session1, true, TRACKS, 10) connection1.errors.size.should == 0 @@ -566,6 +568,5 @@ describe ConnectionManager, no_transaction: true do # connection = @connman.join_music_session(user, client_id, music_session, true, TRACKS) # connection.errors.size.should == 0 end - end diff --git a/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb b/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb new file mode 100644 index 000000000..2f18f774e --- /dev/null +++ b/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb @@ -0,0 +1,497 @@ +require 'spec_helper' + +describe "Monthly Recurring Lesson Flow" do + + let(:user) { FactoryGirl.create(:user, remaining_test_drives: 0) } + + let(:teacher_user) { FactoryGirl.create(:teacher_user) } + let(:teacher) { teacher_user.teacher } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] } + let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } + let(:affiliate_partner) { FactoryGirl.create(:affiliate_partner) } + let(:affiliate_partner2) { FactoryGirl.create(:affiliate_partner, lesson_rate: 0.30) } + let(:school) {FactoryGirl.create(:school)} + + + + after {Timecop.return} + + before { + teacher.stripe_account_id = stripe_account1_id + teacher.save! + } + it "works" do + + # if it's later in the month, we'll make 2 lesson_package_purchases (prorated one, and next month's), which can throw off some assertions later on + Timecop.travel(Date.new(2016, 3, 20)) + + # user has no test drives, no credit card on file, but attempts to book a lesson + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.errors.any?.should be_false + booking.card_presumed_ok.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_normal_lesson + booking.sent_notices.should be_false + booking.booked_price.should eql 30.00 + + ########## Need validate their credit card + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', normal: true, booking_id: booking.id}) + booking.reload + booking.card_presumed_ok.should be_true + booking.errors.any?.should be_false + booking = result[:lesson] + lesson = booking.lesson_sessions[0] + lesson.errors.any?.should be_false + + booking.sent_notices.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + lesson.amount_charged.should be 0.0 + lesson.reload + + user.reload + user.stripe_customer_id.should_not be nil + user.remaining_test_drives.should eql 0 + user.lesson_purchases.length.should eql 0 + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + booking.lesson_sessions.length.should eql 1 + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonBooking::STATUS_REQUESTED + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######### Teacher counters with new slot + teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_recurring, hour: 14, update_all: true) + UserMailer.deliveries.clear + lesson_session.counter({proposer: teacher_user, slot: teacher_countered_slot, message: 'Does this work?'}) + booking.reload + booking.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 1 + lesson_session.lesson_booking_slots[0].proposer.should eql teacher_user + teacher_counter = lesson_session.lesson_booking_slots.order(:created_at).last + teacher_counter.should eql teacher_countered_slot + teacher_counter.proposer.should eql teacher_user + booking.lesson_booking_slots.length.should eql 3 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.message.should eql 'Does this work?' + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######### Student counters with new slot + student_countered_slot = FactoryGirl.build(:lesson_booking_slot_recurring, hour: 16, update_all: true) + UserMailer.deliveries.clear + lesson_session.counter({proposer: user, slot: student_countered_slot, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 2 + student_counter = booking.lesson_booking_slots.order(:created_at).last + student_counter.proposer.should eql user + booking.reload + booking.lesson_booking_slots.length.should eql 4 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Does this work better?' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql user + chat.target_user.should eql teacher_user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql false + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######## Teacher accepts slot + UserMailer.deliveries.clear + lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false, accepter: teacher_user}) + UserMailer.deliveries.each do |del| + # puts del.inspect + end + # get acceptance emails, as well as 'your stuff is accepted' + UserMailer.deliveries.length.should eql 2 + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.slot.should eql student_counter + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.default_slot.should eql student_counter + lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.status.should eql LessonBooking::STATUS_APPROVED + + UserMailer.deliveries.length.should eql 2 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Yeah I got this' + chat.purpose.should eql 'Lesson Approved' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'accept' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + Timecop.travel(end_time + 1) + + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = lesson_session.analysis + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + if lesson_session.billing_error_detail + puts "monthly recurring lesson flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests + end + + # let user pay for it + LessonBooking.hourly_check + + user.reload + user.lesson_purchases.length.should eql 1 + lesson_purchase = user.lesson_purchases[0] + puts "LESSON_PURCHASE PRICE #{lesson_purchase.price}" + lesson_purchase.price.should eql 30.00 + lesson_purchase.lesson_package_type.is_normal?.should eql true + lesson_purchase.price_in_cents.should eql 3000 + teacher_distribution = lesson_purchase.teacher_distribution + teacher_distribution.amount_in_cents.should eql 3000 + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_false + user.sales.length.should eql 1 + sale = user.sales.first + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should eql (100 * booking.booked_price.to_f * 0.0825).round.to_i + sale.recurly_total_in_cents.should eql ((100 * booking.booked_price.to_f * 0.0825).round + 100 * booking.booked_price.to_f).to_i + sale.recurly_subtotal_in_cents.should eql (100 * booking.booked_price).to_i + sale.recurly_currency.should eql 'USD' + sale.stripe_charge_id.should_not be_nil + line_item = sale.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.single.id + line_item.lesson_package_purchase.should eql lesson_purchase + lesson_purchase.sale_line_item.should eql line_item + + TeacherPayment.count.should eql 0 + TeacherPayment.hourly_check + teacher_distribution.reload + teacher_distribution.distributed.should be_true + TeacherPayment.count.should eql 1 + payment = TeacherPayment.first + payment.amount_in_cents.should eql 3000 + payment.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher_payment_charge.amount_in_cents.should eql (3000 + 3000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher.should eql teacher_user + payment.teacher_distribution.should eql teacher_distribution + + + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + + UserMailer.deliveries.clear + # background code comes around and analyses the session + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = lesson_session.analysis + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + if lesson_session.billing_error_detail + puts "monthly recurring lesson flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests + end + + lesson.amount_charged.should eql 0.0 + lesson_session.billing_error_reason.should be_nil + lesson_session.sent_billing_notices.should be nil + user.reload + user.remaining_test_drives.should eql 0 + UserMailer.deliveries.length.should eql 0 # one for student + end + + it "works (school on school)" do + + # get user and teacher into same school + user.school = school + user.save! + teacher.school = school + teacher.save! + + # if it's later in the month, we'll make 2 lesson_package_purchases (prorated one, and next month's), which can throw off some assertions later on + Timecop.travel(Date.new(2016, 3, 20)) + + # user has no test drives, no credit card on file, but attempts to book a lesson + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.errors.any?.should be_false + booking.card_presumed_ok.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.sent_notices.should be_true + booking.booked_price.should eql 30.00 + + + user.reload + user.stripe_customer_id.should be nil + user.remaining_test_drives.should eql 0 + user.lesson_purchases.length.should eql 0 + + booking.lesson_sessions.length.should eql 1 + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonBooking::STATUS_REQUESTED + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######### Teacher counters with new slot + teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_recurring, hour: 14, update_all: true) + UserMailer.deliveries.clear + lesson_session.counter({proposer: teacher_user, slot: teacher_countered_slot, message: 'Does this work?'}) + lesson_session.errors.any?.should be_false + booking.reload + booking.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 1 + lesson_session.lesson_booking_slots[0].proposer.should eql teacher_user + teacher_counter = lesson_session.lesson_booking_slots.order(:created_at).last + teacher_counter.should eql teacher_countered_slot + teacher_counter.proposer.should eql teacher_user + booking.lesson_booking_slots.length.should eql 3 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.message.should eql 'Does this work?' + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######### Student counters with new slot + student_countered_slot = FactoryGirl.build(:lesson_booking_slot_recurring, hour: 16, update_all: true) + UserMailer.deliveries.clear + lesson_session.counter({proposer: user, slot: student_countered_slot, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 2 + student_counter = booking.lesson_booking_slots.order(:created_at).last + student_counter.proposer.should eql user + booking.reload + booking.lesson_booking_slots.length.should eql 4 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Does this work better?' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql user + chat.target_user.should eql teacher_user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql false + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######## Teacher accepts slot + UserMailer.deliveries.clear + lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false, accepter: teacher_user}) + UserMailer.deliveries.each do |del| + # puts del.inspect + end + # get acceptance emails, as well as 'your stuff is accepted' + UserMailer.deliveries.length.should eql 2 + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.slot.should eql student_counter + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.default_slot.should eql student_counter + lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.status.should eql LessonBooking::STATUS_APPROVED + + UserMailer.deliveries.length.should eql 2 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Yeah I got this' + chat.purpose.should eql 'Lesson Approved' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'accept' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + Timecop.travel(end_time + 1) + + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = lesson_session.analysis + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + if lesson_session.billing_error_detail + puts "monthly recurring lesson flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests + end + + # let user pay for it + LessonBooking.hourly_check + + LessonPaymentCharge.count.should eql 0 + TeacherDistribution.count.should eql 0 + + user.reload + user.lesson_purchases.length.should eql 0 + user.sales.length.should eql 0 + + + TeacherPayment.count.should eql 0 + TeacherPayment.hourly_check + TeacherPayment.count.should eql 0 + + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + + UserMailer.deliveries.clear + # background code comes around and analyses the session + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = lesson_session.analysis + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + if lesson_session.billing_error_detail + puts "monthly recurring lesson flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests + end + + lesson_session.amount_charged.should eql 0.0 + lesson_session.billing_error_reason.should be_nil + lesson_session.sent_billing_notices.should be_nil + user.reload + user.remaining_test_drives.should eql 0 + UserMailer.deliveries.length.should eql 0 # one for student + + LessonPaymentCharge.count.should eql 0 + TeacherDistribution.count.should eql 0 + end + + + it "affiliate gets their cut" do + Timecop.travel(2016, 05, 15) + user.affiliate_referral = affiliate_partner + user.save! + teacher_user.affiliate_referral = affiliate_partner2 + teacher_user.save! + + user.lesson_purchases.count.should eql 0 + lesson = monthly_lesson(user, teacher_user, {finish:true, accept:true}) + + user.reload + + user.lesson_purchases.count.should eql 1 + lesson_package_purchase = user.lesson_purchases.first + teacher_distribution = lesson_package_purchase.teacher_distribution + + + user.sales.count.should eql 1 + user.sales[0].sale_line_items[0].affiliate_distributions.count.should eql 2 + affiliate_partner.affiliate_distributions.count.should eql 1 + affiliate_partner2.affiliate_distributions.count.should eql 1 + partner1_distribution = affiliate_partner.affiliate_distributions.first + partner2_distribution = affiliate_partner2.affiliate_distributions.first + partner1_distribution.sale_line_item.should eql partner2_distribution.sale_line_item + partner1_distribution.affiliate_referral_fee_in_cents.should eql (teacher_distribution.jamkazam_margin_in_cents * affiliate_partner.lesson_rate).round + partner2_distribution.affiliate_referral_fee_in_cents.should eql (teacher_distribution.jamkazam_margin_in_cents * affiliate_partner2.lesson_rate).round + end + + it "school affiliate gets nothing when teacher school is involved" do + Timecop.travel(2016, 05, 15) + teacher.school = school + teacher.save! + + user.affiliate_referral = affiliate_partner + user.save! + + lesson = monthly_lesson(user, teacher_user, {finish:true, accept:true }) + + user.sales.count.should eql 1 + user.sales[0].sale_line_items[0].affiliate_distributions.count.should eql 1 + user.lesson_purchases.count.should eql 1 + lesson_package_purchase = user.lesson_purchases.first + teacher_distribution = lesson_package_purchase.teacher_distribution + + + affiliate_partner.affiliate_distributions.count.should eql 1 + partner1_distribution = affiliate_partner.affiliate_distributions.first + partner1_distribution.affiliate_referral_fee_in_cents.should eql (teacher_distribution.jamkazam_margin_in_cents * affiliate_partner.lesson_rate).round + school.affiliate_partner.affiliate_distributions.count.should eql 0 + end + + it "student school affiliates gets cut when student school is involved. so does teacher's" do + # in the middle of the month so that we don't get the next month's in-advance purchase put on us + Timecop.travel(2016, 05, 15) + user.affiliate_referral = school.affiliate_partner + user.save! + + teacher_user.affiliate_referral = affiliate_partner + teacher_user.save! + + lesson = monthly_lesson(user, teacher_user, {finish: true, accept: true}) + + user.sales.count.should eql 1 + user.sales[0].sale_line_items[0].affiliate_distributions.count.should eql 2 + user.lesson_purchases.count.should eql 1 + lesson_package_purchase = user.lesson_purchases.first + teacher_distribution = lesson_package_purchase.teacher_distribution + + + affiliate_partner.affiliate_distributions.count.should eql 1 + partner1_distribution = affiliate_partner.affiliate_distributions.count.should eql 1 + school.affiliate_partner.affiliate_distributions.count.should eql 1 + school_partner_distribution = school.affiliate_partner.affiliate_distributions.first + school_partner_distribution.affiliate_referral_fee_in_cents.should eql (teacher_distribution.jamkazam_margin_in_cents * school.affiliate_partner.lesson_rate).round + end +end diff --git a/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb b/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb new file mode 100644 index 000000000..927db8788 --- /dev/null +++ b/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb @@ -0,0 +1,627 @@ +require 'spec_helper' + +describe "Normal Lesson Flow" do + + let(:user) { FactoryGirl.create(:user, remaining_test_drives: 0) } + + let(:teacher_user) { FactoryGirl.create(:teacher_user) } + let(:teacher) { teacher_user.teacher } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] } + let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } + let(:affiliate_partner) { FactoryGirl.create(:affiliate_partner) } + let(:affiliate_partner2) { FactoryGirl.create(:affiliate_partner, lesson_rate: 0.30) } + let(:school) {FactoryGirl.create(:school)} + + after {Timecop.return} + + describe "stripe mocked" do + before { + StripeMock.clear_errors + StripeMock.start + teacher.stripe_account_id = stripe_account1_id + teacher.save! + } + after { StripeMock.stop } + + it "bill failure" do + + # user has no test drives, no credit card on file, but attempts to book a lesson + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + booking.errors.any?.should be_false + booking.card_presumed_ok.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_normal_lesson + booking.sent_notices.should be_false + booking.booked_price.should eql 30.00 + + ########## Need validate their credit card + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', normal: true, booking_id: booking.id}) + booking = result[:lesson] + lesson = booking.lesson_sessions[0] + booking.errors.any?.should be_false + lesson.errors.any?.should be_false + booking.card_presumed_ok.should be_true + booking.sent_notices.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + lesson.amount_charged.should be 0.0 + lesson.reload + + user.reload + user.stripe_customer_id.should_not be nil + user.remaining_test_drives.should eql 0 + user.lesson_purchases.length.should eql 0 + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + booking.lesson_sessions.length.should eql 1 + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonBooking::STATUS_REQUESTED + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######## Teacher accepts slot + UserMailer.deliveries.clear + + lesson_session.accept({message: 'Yeah I got this', slot: booking.default_slot.id, update_all: false, accepter: teacher_user}) + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.slot.should eql booking.default_slot + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.status.should eql LessonBooking::STATUS_APPROVED + + UserMailer.deliveries.length.should eql 2 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Yeah I got this' + chat.purpose.should eql 'Lesson Approved' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'accept' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + Timecop.travel(end_time + 1) + + UserMailer.deliveries.clear + # background code comes around and analyses the session + + StripeMock.prepare_card_error(:card_declined) + + lesson_session.lesson_payment_charge.billing_attempts.should eql 0 + user.lesson_purchases.length.should eql 0 + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = lesson_session.analysis + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + lesson_session.billing_attempts.should eql 1 + lesson_session.billing_error_reason.should eql 'card_declined' + lesson_session.billed.should eql false + lesson_session.billing_attempts.should eql 1 + user.reload + user.lesson_purchases.length.should eql 0 + + LessonBooking.hourly_check + + lesson_session.reload + teacher_distribution = lesson_session.teacher_distribution + teacher_distribution.amount_in_cents.should eql 3000 + teacher_distribution.ready.should be_false + teacher_distribution.distributed.should be_false + + + # let's reattempt right away; this should have no effect because we only try to bill once per 24 hours + + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = lesson_session.analysis + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + lesson_session.billing_error_reason.should eql 'card_declined' + lesson_session.billed.should eql false + lesson_session.billing_attempts.should eql 1 + user.reload + user.lesson_purchases.length.should eql 0 + + Timecop.freeze((24 + 1).hours.from_now) + StripeMock.clear_errors + StripeMock.prepare_card_error(:expired_card) + + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = lesson_session.analysis + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + lesson_session.billing_attempts.should eql 2 + lesson_session.billing_error_reason.should eql 'card_expired' + lesson_session.billed.should eql false + user.reload + user.lesson_purchases.length.should eql 0 + + Timecop.freeze((24 + 24 + 2).hours.from_now) + StripeMock.clear_errors + StripeMock.prepare_card_error(:processing_error) + + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = lesson_session.analysis + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + lesson_session.billing_attempts.should eql 3 + lesson_session.billing_error_reason.should eql 'processing_error' + lesson_session.billed.should eql false + user.reload + user.lesson_purchases.length.should eql 0 + + + Timecop.freeze((24 + 24 + 24 + 3).hours.from_now) + StripeMock.clear_errors + + # finally the user will get billed! + LessonSession.hourly_check + + + lesson_session.reload + payment = lesson_session.lesson_payment_charge + payment.amount_in_cents.should eql 3248 + payment.fee_in_cents.should eql 0 + + lesson_session.billing_attempts.should eql 4 + lesson_session.post_processed.should be_true + LessonPaymentCharge.count.should eql 1 + + lesson_session.reload + lesson_session.analysed.should be_true + analysis = lesson_session.analysis + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + lesson_session.billing_attempts.should eql 4 + if lesson_session.billing_error_detail + lesson_session.billing_error_detail + end + lesson_session.billing_error_reason.should eql 'processing_error' + lesson_session.billed.should eql true + user.reload + user.lesson_purchases.length.should eql 1 + + + LessonBooking.hourly_check + payment.reload + payment.amount_in_cents.should eql 3248 + payment.fee_in_cents.should eql 0 + lesson_session.reload + teacher_distribution = lesson_session.teacher_distribution + teacher_distribution.amount_in_cents.should eql 3000 + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_false + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 30.00 + lesson_purchase.lesson_package_type.is_normal?.should eql true + lesson_purchase.price_in_cents.should eql 3000 + user.sales.length.should eql 1 + sale = user.sales.first + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should eql (100 * booking.booked_price.to_f * 0.0825).round.to_i + sale.recurly_total_in_cents.should eql ((100 * booking.booked_price.to_f * 0.0825).round + 100 * booking.booked_price.to_f).to_i + sale.recurly_subtotal_in_cents.should eql (100 * booking.booked_price).to_i + sale.recurly_currency.should eql 'USD' + sale.stripe_charge_id.should_not be_nil + line_item = sale.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.single.id + line_item.lesson_package_purchase.should eql lesson_purchase + lesson_purchase.sale_line_item.should eql line_item + lesson.reload + lesson.amount_charged.should eql (sale.recurly_total_in_cents / 100.0).to_f + lesson_session.billing_error_reason.should eql 'processing_error' + lesson_session.sent_billing_notices.should be true + user.reload + user.remaining_test_drives.should eql 0 + UserMailer.deliveries.length.should eql 2 # one for student, one for teacher + + TeacherPayment.count.should eql 0 + TeacherPayment.hourly_check + TeacherPayment.count.should eql 1 + teacher_distribution.reload + teacher_distribution.distributed.should be_true + + payment = TeacherPayment.first + payment.amount_in_cents.should eql 3000 + payment.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher_payment_charge.amount_in_cents.should eql (3000 + 3000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher.should eql teacher_user + payment.teacher_distribution.should eql teacher_distribution + lesson_session.lesson_booking.status.should eql LessonBooking::STATUS_COMPLETED + lesson_session.lesson_booking.success.should be_true + lesson_session.teacher.has_booked_test_drive_with_student?(user).should be_false + + end + end + + + it "works" do + + # user has no test drives, no credit card on file, but attempts to book a lesson + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + booking.errors.any?.should be_false + booking.card_presumed_ok.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_normal_lesson + booking.sent_notices.should be_false + booking.booked_price.should eql 30.00 + + ########## Need validate their credit card + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', normal: true, booking_id: booking.id}) + booking = result[:lesson] + lesson = booking.lesson_sessions[0] + booking.errors.any?.should be_false + lesson.errors.any?.should be_false + booking.card_presumed_ok.should be_true + booking.sent_notices.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + lesson.amount_charged.should eql 0.0 + lesson.reload + + user.reload + user.stripe_customer_id.should_not be nil + user.remaining_test_drives.should eql 0 + user.lesson_purchases.length.should eql 0 + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + booking.lesson_sessions.length.should eql 1 + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonBooking::STATUS_REQUESTED + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######### Teacher counters with new slot + teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 14) + UserMailer.deliveries.clear + lesson_session.counter({proposer: teacher_user, slot: teacher_countered_slot, message: 'Does this work?'}) + booking.reload + booking.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 1 + lesson_session.lesson_booking_slots[0].proposer.should eql teacher_user + teacher_counter = lesson_session.lesson_booking_slots.order(:created_at).last + teacher_counter.should eql teacher_countered_slot + teacher_counter.proposer.should eql teacher_user + booking.lesson_booking_slots.length.should eql 3 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.message.should eql 'Does this work?' + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######### Student counters with new slot + student_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 16) + UserMailer.deliveries.clear + lesson_session.counter({proposer: user, slot: student_countered_slot, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 2 + student_counter = booking.lesson_booking_slots.order(:created_at).last + student_counter.proposer.should eql user + booking.reload + booking.lesson_booking_slots.length.should eql 4 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Does this work better?' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql user + chat.target_user.should eql teacher_user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql false + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######## Teacher accepts slot + UserMailer.deliveries.clear + + lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false,accepter: teacher_user}) + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.slot.should eql student_counter + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.default_slot.should eql student_counter + lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.status.should eql LessonBooking::STATUS_APPROVED + + UserMailer.deliveries.length.should eql 2 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Yeah I got this' + chat.purpose.should eql 'Lesson Approved' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'accept' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + Timecop.travel(end_time + 1) + + UserMailer.deliveries.clear + # background code comes around and analyses the session + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = lesson_session.analysis + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + if lesson_session.billing_error_detail + puts "testdrive flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests + end + lesson_session.billed.should be true + user.reload + user.lesson_purchases.length.should eql 1 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 30.00 + lesson_purchase.lesson_package_type.is_normal?.should eql true + lesson_purchase.price_in_cents.should eql 3000 + user.sales.length.should eql 1 + sale = user.sales.first + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should eql (100 * booking.booked_price.to_f * 0.0825).round.to_i + sale.recurly_total_in_cents.should eql ((100 * booking.booked_price.to_f * 0.0825).round + 100 * booking.booked_price.to_f).to_i + sale.recurly_subtotal_in_cents.should eql (100 * booking.booked_price).to_i + sale.recurly_currency.should eql 'USD' + sale.stripe_charge_id.should_not be_nil + line_item = sale.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.single.id + line_item.lesson_package_purchase.should eql lesson_purchase + lesson_purchase.sale_line_item.should eql line_item + lesson.amount_charged.should eql (sale.recurly_total_in_cents / 100.0).to_f + lesson_session.billing_error_reason.should be_nil + lesson_session.sent_billing_notices.should be true + user.reload + user.remaining_test_drives.should eql 0 + UserMailer.deliveries.length.should eql 2 # one for student, one for teacher + end + + + it "works (school on school)" do + + # get user and teacher into same school + user.school = school + user.save! + teacher.school = school + teacher.save! + + # user has no test drives, no credit card on file, but attempts to book a lesson + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + booking.errors.any?.should be_false + booking.school.should be_true + booking.card_presumed_ok.should be_false + booking.user.should eql user + user.unprocessed_normal_lesson.should be_nil + booking.sent_notices.should be_true + booking.booked_price.should eql 30.00 + booking.is_requested?.should be_true + booking.lesson_sessions[0].music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + LessonPaymentCharge.count.should eql 0 + + user.reload + user.stripe_customer_id.should be_nil + user.remaining_test_drives.should eql 0 + user.lesson_purchases.length.should eql 0 + + booking.lesson_sessions.length.should eql 1 + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonBooking::STATUS_REQUESTED + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######### Teacher counters with new slot + teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 14) + UserMailer.deliveries.clear + lesson_session.counter({proposer: teacher_user, slot: teacher_countered_slot, message: 'Does this work?'}) + booking.reload + booking.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 1 + lesson_session.lesson_booking_slots[0].proposer.should eql teacher_user + teacher_counter = lesson_session.lesson_booking_slots.order(:created_at).last + teacher_counter.should eql teacher_countered_slot + teacher_counter.proposer.should eql teacher_user + booking.lesson_booking_slots.length.should eql 3 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.message.should eql 'Does this work?' + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######### Student counters with new slot + student_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 16) + UserMailer.deliveries.clear + lesson_session.counter({proposer: user, slot: student_countered_slot, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 2 + student_counter = booking.lesson_booking_slots.order(:created_at).last + student_counter.proposer.should eql user + booking.reload + booking.lesson_booking_slots.length.should eql 4 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Does this work better?' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql user + chat.target_user.should eql teacher_user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql false + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######## Teacher accepts slot + UserMailer.deliveries.clear + + lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false, accepter: teacher_user}) + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.slot.should eql student_counter + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.default_slot.should eql student_counter + lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.status.should eql LessonBooking::STATUS_APPROVED + + UserMailer.deliveries.length.should eql 2 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Yeah I got this' + chat.purpose.should eql 'Lesson Approved' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'accept' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + Timecop.travel(end_time + 1) + + UserMailer.deliveries.clear + # background code comes around and analyses the session + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = lesson_session.analysis + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + if lesson_session.billing_error_detail + puts "testdrive flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests + end + lesson_session.billed.should be false + user.reload + user.lesson_purchases.length.should eql 0 + user.sales.length.should eql 0 + lesson_session.amount_charged.should eql 0.0 + lesson_session.billing_error_reason.should be_nil + lesson_session.sent_billing_notices.should be_nil + user.reload + user.remaining_test_drives.should eql 0 + UserMailer.deliveries.length.should eql 0 # one for student, one for teacher + + LessonPaymentCharge.count.should eql 0 + TeacherDistribution.count.should eql 0 + end + + it "affiliate gets their cut" do + user.affiliate_referral = affiliate_partner + user.save! + teacher_user.affiliate_referral = affiliate_partner2 + teacher_user.save! + + lesson = normal_lesson(user, teacher_user, {accept: true, finish:true}) + + user.reload + user.sales.count.should eql 1 + user.sales[0].sale_line_items[0].affiliate_distributions.count.should eql 2 + affiliate_partner.affiliate_distributions.count.should eql 1 + affiliate_partner2.affiliate_distributions.count.should eql 1 + partner1_distribution = affiliate_partner.affiliate_distributions.first + partner2_distribution = affiliate_partner2.affiliate_distributions.first + partner1_distribution.sale_line_item.should eql partner2_distribution.sale_line_item + partner1_distribution.affiliate_referral_fee_in_cents.should eql (3000 * 0.25 * affiliate_partner.lesson_rate).round + partner2_distribution.affiliate_referral_fee_in_cents.should eql (3000 * 0.25 * affiliate_partner2.lesson_rate).round + end + + it "school affiliate gets nothing when teacher school is involved" do + teacher.school = school + teacher.save! + + user.affiliate_referral = affiliate_partner + user.save! + + lesson = normal_lesson(user, teacher_user, {accept: true, finish:true}) + lesson.errors.any?.should be_false + + user.sales.count.should eql 1 + user.sales[0].sale_line_items[0].affiliate_distributions.count.should eql 1 + + affiliate_partner.affiliate_distributions.count.should eql 1 + partner1_distribution = affiliate_partner.affiliate_distributions.first + partner1_distribution.affiliate_referral_fee_in_cents.should eql (3000 * 0.25 * affiliate_partner.lesson_rate).round + school.affiliate_partner.affiliate_distributions.count.should eql 0 + end + + it "student school affiliates gets cut when student school is involved. so does teacher's" do + user.affiliate_referral = school.affiliate_partner + user.save! + + teacher_user.affiliate_referral = affiliate_partner + teacher_user.save! + + lesson = normal_lesson(user, teacher_user, {accept: true, finish:true}) + + user.sales.count.should eql 1 + user.sales[0].sale_line_items[0].affiliate_distributions.count.should eql 2 + + affiliate_partner.affiliate_distributions.count.should eql 1 + partner1_distribution = affiliate_partner.affiliate_distributions.count.should eql 1 + school.affiliate_partner.affiliate_distributions.count.should eql 1 + school_partner_distribution = school.affiliate_partner.affiliate_distributions.first + school_partner_distribution.affiliate_referral_fee_in_cents.should eql (3000 * 0.25 * school.affiliate_partner.lesson_rate).round + end +end diff --git a/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb b/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb new file mode 100644 index 000000000..46265e218 --- /dev/null +++ b/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb @@ -0,0 +1,193 @@ +require 'spec_helper' + +describe "Recurring Lesson Flow" do + + let(:user) { FactoryGirl.create(:user, remaining_test_drives: 0) } + + let(:teacher_user) { FactoryGirl.create(:teacher_user) } + let(:teacher) { teacher_user.teacher } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] } + let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } + + after(:each) do + Timecop.return + end + it "works" do + + # user has no test drives, no credit card on file, but attempts to book a lesson + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + booking.errors.any?.should be_false + booking.card_presumed_ok.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_normal_lesson + booking.sent_notices.should be_false + booking.booked_price.should eql 30.00 + + ########## Need validate their credit card + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', normal: true, booking_id: booking.id}) + booking = result[:lesson] + lesson = booking.lesson_sessions[0] + booking.errors.any?.should be_false + lesson.errors.any?.should be_false + booking.card_presumed_ok.should be_true + booking.sent_notices.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + lesson.amount_charged.should be 0.0 + lesson.reload + + user.reload + user.stripe_customer_id.should_not be nil + user.remaining_test_drives.should eql 0 + user.lesson_purchases.length.should eql 0 + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + booking.lesson_sessions.length.should eql 1 + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonBooking::STATUS_REQUESTED + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######### Teacher counters with new slot + teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_recurring, hour: 14, update_all: true) + UserMailer.deliveries.clear + lesson_session.counter({proposer: teacher_user, slot: teacher_countered_slot, message: 'Does this work?'}) + booking.reload + booking.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 1 + lesson_session.lesson_booking_slots[0].proposer.should eql teacher_user + teacher_counter = lesson_session.lesson_booking_slots.order(:created_at).last + teacher_counter.should eql teacher_countered_slot + teacher_counter.proposer.should eql teacher_user + booking.lesson_booking_slots.length.should eql 3 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.message.should eql 'Does this work?' + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + #notification.message.should eql "Instructor has proposed a different time for your lesson." + + ######### Student counters with new slot + student_countered_slot = FactoryGirl.build(:lesson_booking_slot_recurring, hour: 16, update_all: true) + UserMailer.deliveries.clear + lesson_session.counter({proposer: user, slot: student_countered_slot, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 2 + student_counter = booking.lesson_booking_slots.order(:created_at).last + student_counter.proposer.should eql user + booking.reload + booking.lesson_booking_slots.length.should eql 4 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Does this work better?' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql user + chat.target_user.should eql teacher_user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql false + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######## Teacher accepts slot + UserMailer.deliveries.clear + lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, accepter: teacher_user}) + UserMailer.deliveries.each do |del| + # puts del.inspect + end + # get acceptance emails, as well as 'your stuff is accepted' + UserMailer.deliveries.length.should eql 2 + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.slot.should eql student_counter + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.default_slot.should eql student_counter + lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.status.should eql LessonBooking::STATUS_APPROVED + + UserMailer.deliveries.length.should eql 2 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql "Yeah I got this" + chat.purpose.should eql 'Lesson Approved' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'accept' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + user.reload + user.sales.length.should eql 0 + + booking.reload + booking.lesson_sessions[0].scheduled_start.should_not eql booking.lesson_sessions[1].scheduled_start + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + Timecop.travel(end_time + 1) + + UserMailer.deliveries.clear + # background code comes around and analyses the session + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = lesson_session.analysis + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + if lesson_session.billing_error_detail + puts "testdrive flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests + end + lesson_session.billed.should be true + user.reload + user.lesson_purchases.length.should eql 1 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 30.00 + lesson_purchase.lesson_package_type.is_normal?.should eql true + lesson_purchase.price_in_cents.should eql 3000 + user.sales.length.should eql 1 + sale = user.sales.first + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should eql (100 * booking.booked_price.to_f * 0.0825).round.to_i + sale.recurly_total_in_cents.should eql ((100 * booking.booked_price.to_f * 0.0825).round + 100 * booking.booked_price.to_f).to_i + sale.recurly_subtotal_in_cents.should eql (100 * booking.booked_price).to_i + sale.recurly_currency.should eql 'USD' + sale.stripe_charge_id.should_not be_nil + line_item = sale.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.single.id + line_item.lesson_package_purchase.should eql lesson_purchase + lesson_purchase.sale_line_item.should eql line_item + lesson.amount_charged.should eql (sale.recurly_total_in_cents / 100.0).to_f + lesson_session.billing_error_reason.should be_nil + lesson_session.sent_billing_notices.should be true + user.reload + user.remaining_test_drives.should eql 0 + UserMailer.deliveries.each do |d| + puts d.subject + end + UserMailer.deliveries.length.should eql 2 # one for student, one for teacher + end +end diff --git a/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb b/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb new file mode 100644 index 000000000..28bc1fb40 --- /dev/null +++ b/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb @@ -0,0 +1,380 @@ +require 'spec_helper' + +describe "TestDrive Lesson Flow" do + + let(:user) { FactoryGirl.create(:user, remaining_test_drives: 0) } + + let(:teacher_user) { FactoryGirl.create(:teacher_user) } + let(:teacher_user2) { FactoryGirl.create(:teacher_user) } + let(:teacher) { teacher_user.teacher } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] } + let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } + let(:affiliate_partner) { FactoryGirl.create(:affiliate_partner) } + let(:affiliate_partner2) { FactoryGirl.create(:affiliate_partner, lesson_rate: 0.30) } + let(:school) { FactoryGirl.create(:school) } + + + before { + teacher.stripe_account_id = stripe_account1_id + teacher.save! + } + after { + Timecop.return + } + + it "works" do + + user.desired_package = LessonPackageType.test_drive_2 + user.save! + + # user has no test drives, no credit card on file, but attempts to book a lesson + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be_false + booking.card_presumed_ok.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_test_drive + booking.sent_notices.should be_false + teacher_user.has_booked_test_drive_with_student?(user).should be_true + + user.reload + user.remaining_test_drives.should eql 0 + + ########## Need validate their credit card + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', test_drive: true, booking_id: booking.id}) + booking = result[:lesson] + booking.errors.any?.should be_false + lesson = booking.lesson_sessions[0] + + lesson.errors.any?.should be_false + test_drive = result[:test_drive] + test_drive.errors.any?.should be_false + + user.reload + user.remaining_test_drives.should eql 1 + + booking.card_presumed_ok.should be_true + booking.sent_notices.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + lesson.reload + + + test_drive.stripe_charge_id.should_not be_nil + test_drive.recurly_tax_in_cents.should be 247 + test_drive.recurly_total_in_cents.should eql 2999 + 247 + test_drive.recurly_subtotal_in_cents.should eql 2999 + test_drive.recurly_currency.should eql 'USD' + line_item = test_drive.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.test_drive_2.id + user.reload + user.stripe_customer_id.should_not be nil + user.lesson_purchases.length.should eql 1 + user.remaining_test_drives.should eql 1 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 29.99 + lesson_purchase.lesson_package_type.is_test_drive?.should eql true + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + booking.lesson_sessions.length.should eql 1 + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonBooking::STATUS_REQUESTED + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######### Teacher counters with new slot + teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 14) + UserMailer.deliveries.clear + lesson_session.counter({proposer: teacher_user, slot: teacher_countered_slot, message: 'Does this work?'}) + booking.reload + booking.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 1 + lesson_session.lesson_booking_slots[0].proposer.should eql teacher_user + teacher_counter = lesson_session.lesson_booking_slots.order(:created_at).last + teacher_counter.should eql teacher_countered_slot + teacher_counter.proposer.should eql teacher_user + booking.lesson_booking_slots.length.should eql 3 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.message.should eql 'Does this work?' + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######### Student counters with new slot + student_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 16) + UserMailer.deliveries.clear + lesson_session.counter({proposer: user, slot: student_countered_slot, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 2 + student_counter = booking.lesson_booking_slots.order(:created_at).last + student_counter.proposer.should eql user + booking.reload + booking.lesson_booking_slots.length.should eql 4 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Does this work better?' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql user + chat.target_user.should eql teacher_user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql false + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######## Teacher accepts slot + UserMailer.deliveries.clear + + lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false, accepter: teacher_user}) + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.slot.should eql student_counter + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.default_slot.should eql student_counter + lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.status.should eql LessonBooking::STATUS_APPROVED + + UserMailer.deliveries.length.should eql 2 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Yeah I got this' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'accept' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + notification.message.should be_nil + + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + Timecop.travel(end_time + 1) + + + UserMailer.deliveries.clear + # background code comes around and analyses the session + lesson.analyse + lesson.session_completed + lesson_session.reload + lesson_session.analysed.should be_true + analysis = lesson_session.analysis + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + lesson_session.billed.should be false + if lesson_session.billing_error_detail + puts "testdrive flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests + end + lesson_session.billing_error_reason.should be_nil + lesson_session.sent_notices.should be true + purchase = lesson_session.lesson_package_purchase + purchase.should_not be_nil + purchase.price_in_cents.should eql 2999 + purchase.lesson_package_type.is_test_drive?.should be true + user.reload + user.remaining_test_drives.should eql 1 + UserMailer.deliveries.length.should eql 2 # one for student, one for teacher + found_student_email = false + UserMailer.deliveries.each do |d| + if d.subject == "You have used 1 of 2 TestDrive lesson credits" + found_student_email = true + end + end + found_student_email.should be_true + + teacher_distribution = lesson_session.teacher_distribution + teacher_distribution.amount_in_cents.should eql 1000 + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_false + + LessonBooking.hourly_check + LessonSession.hourly_check + + teacher_distribution.reload + teacher_distribution.amount_in_cents.should eql 1000 + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_false + + TeacherPayment.count.should eql 0 + TeacherPayment.hourly_check + TeacherPayment.count.should eql 1 + + lesson_session.reload + purchase.reload + + purchase.teacher_distribution.should be_nil + + teacher_payment = TeacherPayment.first + teacher_payment.amount_in_cents.should eql 1000 + teacher_payment.fee_in_cents.should eql 0 + teacher_payment.teacher.should eql teacher_user + + teacher_distribution.reload + teacher_distribution.amount_in_cents.should eql 1000 + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_true + + teacher_payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + teacher_payment.teacher_payment_charge.fee_in_cents.should eql 0 + + user.sales.count.should eql 1 + sale = user.sales[0] + sale.sale_line_items.count.should eql 1 + sale.sale_line_items[0].affiliate_distributions.count.should eql 0 + + lesson_session.lesson_booking.status.should eql LessonBooking::STATUS_COMPLETED + lesson_session.lesson_booking.success.should be_true + LessonBooking.bookings(user, teacher_user, nil).count.should eql 1 + LessonBooking.engaged_bookings(user, teacher_user, nil).count.should eql 1 + teacher_user.has_booked_test_drive_with_student?(user).should be_true + + + end + + # VRFS-4069 + it "cancels with no credit card" do + + slots = [] + slots << FactoryGirl.build(:lesson_booking_slot_single) + slots << FactoryGirl.build(:lesson_booking_slot_single) + + booking = LessonBooking.book_test_drive(user, teacher_user, slots, "Hey I've heard of you before.") + + booking.errors.any?.should be_false + lesson = booking.lesson_sessions[0] + booking.card_presumed_ok.should be_false + + user.reload + user.remaining_test_drives.should eql 0 + + lesson.cancel({canceler: user, message: "sorry about that"}) + + user.reload + + user.remaining_test_drives.should eql 0 + booking.reload + booking.status.should eql LessonBooking::STATUS_CANCELED + teacher_user.has_booked_test_drive_with_student?(user).should be_false + end + + it "cancels with credit card" do + lesson = testdrive_lesson(user, teacher_user) + + user.reload + user.remaining_test_drives.should eql 3 + + lesson.cancel({canceler: user, message: "sorry about that"}) + + user.reload + + user.remaining_test_drives.should eql 4 + end + + it "post-lesson emails" do + UserMailer.deliveries.clear + lesson = testdrive_lesson(user, teacher_user, {accept:true, success: true}) + + user.reload + user.remaining_test_drives.should eql 3 + + found_student_email = false + UserMailer.deliveries.each do |d| + if d.subject == "You have used 1 of 4 TestDrive lesson credits" + found_student_email = true + end + end + found_student_email.should be_true + end + + # tests that the correct emails are sent out after test drives are done + it "both no show, then student no shows" do + UserMailer.deliveries.clear + lesson = testdrive_lesson(user, teacher_user, {accept: true, miss: true, package_count: 2}) + found_no_credit_email = false + found_no_success_email = false + UserMailer.deliveries.each do |d| + if d.subject == "Your TestDrive with #{teacher_user.name} will not use a credit" + found_no_credit_email = true + end + if d.subject == "Your TestDrive with #{user.name} was not successful" + found_no_success_email = true + end + end + + found_no_credit_email.should be_true + found_no_success_email.should be_true + + UserMailer.deliveries.clear + user.reload + user.remaining_test_drives.should eql 2 + + lesson = testdrive_lesson(user, teacher_user, {accept: true, finish: true}) + + user.reload + user.remaining_test_drives.should eql 1 + + completed_test_drive = false + done_not_completed = false + paid_but_no_show = false + UserMailer.deliveries.each do |d| + if d.subject == "You have used #{user.remaining_test_drives} of #{user.total_test_drives} TestDrive lesson credits" + done_not_completed = true + end + if d.subject == "You have used all TestDrive lesson credits" + completed_test_drive = true + end + if d.subject == "You successfully completed a lesson with #{user.name}" + paid_but_no_show = true + end + end + done_not_completed.should be_true + paid_but_no_show.should be_true + completed_test_drive.should be_false + + UserMailer.deliveries.clear + + # now use last credit + lesson = testdrive_lesson(user, teacher_user2, {accept: true, finish: true, student_show: true}) + + user.reload + user.remaining_test_drives.should eql 0 + + completed_test_drive = true + paid = false + UserMailer.deliveries.each do |d| + if d.subject == "You have used all TestDrive lesson credits" + completed_test_drive = true + end + if d.subject == "You successfully completed a lesson with #{user.name}" + paid = true + end + end + + # You successfully completed a lesson with #{@student.name}" + completed_test_drive.should be_true + paid.should be_true + 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 b00cd5efb..d85623f52 100644 --- a/ruby/spec/jam_ruby/models/active_music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/active_music_session_spec.rb @@ -2,12 +2,62 @@ require 'spec_helper' describe ActiveMusicSession do + TRACKS = [{"instrument_id" => "electric guitar", "sound" => "mono", "client_track_id" => "some_client_track_id"}] + before(:each) do ActiveMusicSession.delete_all IcecastServer.delete_all IcecastMount.delete_all end + describe "participant_create" do + + let(:user) {FactoryGirl.create(:user)} + + it "fails gracefully when no connection" do + music_session = FactoryGirl.create(:active_music_session, :creator => user, :musician_access => false) + + begin + ActiveMusicSession.participant_create(user, music_session.id, "junk", true, nil, 5) + false.should be_true + rescue JamRuby::JamRecordNotFound => e + e.record_type.should eql "Connection" + e.missing_message.should eql "Unable to find connection by client_id junk" + end + + end + + it "succeeds no active music session" do + music_session = FactoryGirl.create(:music_session, :creator => user, :musician_access => false) + conn = FactoryGirl.create(:connection, :user => user) + connection = ActiveMusicSession.participant_create(user, music_session.id, conn.client_id, true, nil, 5) + connection.errors.any?.should be false + ActiveMusicSession.find(music_session.id) + end + + it "fails gracefully when invalid music session Id" do + music_session = FactoryGirl.create(:active_music_session, :creator => user, :musician_access => false) + conn = FactoryGirl.create(:connection, :user => user) + expect { ActiveMusicSession.participant_create(user, 'bad music session ID', conn.client_id, true, nil, 5) }.to raise_error(ActiveRecord::RecordNotFound) + end + + it "pulls out of other session" do + music_session1 = FactoryGirl.create(:active_music_session, :creator => user, :musician_access => false) + music_session2 = FactoryGirl.create(:active_music_session, :creator => user, :musician_access => false) + conn = FactoryGirl.create(:connection, :user => user, music_session: music_session2) + connection = ActiveMusicSession.participant_create(user, music_session1.id, conn.client_id, true, nil, 5) + connection.should eql conn + connection.errors.any?.should be true + end + + it "user-less connection bails appropriately" do + music_session1 = FactoryGirl.create(:active_music_session, :creator => user, :musician_access => false) + conn = FactoryGirl.create(:connection) + ActiveRecord::Base.connection.execute("update connections set user_id = NULL where connections.id = '#{conn.id}'") + expect { ActiveMusicSession.participant_create(user, music_session1.id, conn.client_id, true, nil, 5) }.to raise_error(JamRuby::JamPermissionError) + end + end + it 'can grant access to valid user' do user1 = FactoryGirl.create(:user) # in the jam session diff --git a/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb index c8fd40dc0..7876b9bff 100644 --- a/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb +++ b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb @@ -112,6 +112,15 @@ describe AffiliatePartner do user.should_attribute_sale?(shopping_cart).should eq({fee_in_cents:20}) end + it "user with an affiliate relationship (with a custom rate) buying a jamtrack" do + user.affiliate_referral = partner + user.save! + partner.rate = 0.25 + partner.save! + shopping_cart = ShoppingCart.create user, jam_track, 1, false + user.should_attribute_sale?(shopping_cart).should eq({fee_in_cents:50}) + end + it "user with an affiliate relationship redeeming a jamtrack" do user.affiliate_referral = partner user.save! @@ -336,6 +345,8 @@ describe AffiliatePartner do 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.affiliate_distributions.first.created_at = freebie_sale.created_at + freebie_sale.affiliate_distributions.first.save! freebie_sale.save! @@ -356,6 +367,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 1, 1) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) quarter = partner1.quarters.first @@ -371,6 +384,9 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 1, 1) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! + AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) quarter = partner1.quarters.first @@ -386,6 +402,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 1, 1) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) quarter = partner1.quarters.first @@ -417,6 +435,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2014, 12, 31) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) quarter = partner1.quarters.first @@ -432,6 +452,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 4, 1) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! real_sale_later = real_sale AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) @@ -448,6 +470,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 3, 31) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) quarter = partner1.quarters.first @@ -459,6 +483,9 @@ describe AffiliatePartner do real_sale.affiliate_refunded_at = Date.new(2015, 3, 1) real_sale.affiliate_refunded = true real_sale.save! + real_sale.affiliate_distributions.first.affiliate_refunded_at = real_sale.affiliate_refunded_at + real_sale.affiliate_distributions.first.affiliate_refunded = real_sale.affiliate_refunded + real_sale.affiliate_distributions.first.save! AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) quarter = partner1.quarters.first @@ -478,6 +505,9 @@ describe AffiliatePartner do real_sale_later.affiliate_refunded_at = Date.new(2015, 7, 1) real_sale_later.affiliate_refunded = true real_sale_later.save! + real_sale_later.affiliate_distributions.first.affiliate_refunded_at = real_sale_later.affiliate_refunded_at + real_sale_later.affiliate_distributions.first.affiliate_refunded = real_sale_later.affiliate_refunded + real_sale_later.affiliate_distributions.first.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) @@ -549,6 +579,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 4, 1) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! AffiliatePartner.tally_up(Date.new(2015, 4, 1)) AffiliateQuarterlyPayment.count.should eq(3) @@ -593,6 +625,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 1, 1) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! AffiliatePartner.tally_up(Date.new(2015, 4, 2)) quarter = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(0, 2015, partner1.id) @@ -751,6 +785,22 @@ describe AffiliatePartner do end end + describe "edge case" do + it "year change" do + partner.touch + last_day_of_year = Date.new(2015, 12, 31) + first_day_of_next_year = Date.new(2016, 01, 01) + AffiliatePartner.tally_up(last_day_of_year) + AffiliatePartner.tally_up(first_day_of_next_year) + quarterly_payment = AffiliateQuarterlyPayment.where(year: 2016, quarter: 0, affiliate_partner_id: partner.id).first + quarterly_payment.closed.should be_false + AffiliatePartner.tally_up(Date.new(2016, 01, 02)) + quarterly_payment.reload + quarterly_payment.closed.should be_false + end + end + + describe "boundary_dates_for_month" do it "invalid month" do expect{AffiliatePartner.boundary_dates_for_month(2015, 0)}.to raise_error diff --git a/ruby/spec/jam_ruby/models/band_filter_search_spec.rb b/ruby/spec/jam_ruby/models/band_filter_search_spec.rb index 86004bdc7..f23f7c188 100644 --- a/ruby/spec/jam_ruby/models/band_filter_search_spec.rb +++ b/ruby/spec/jam_ruby/models/band_filter_search_spec.rb @@ -213,6 +213,7 @@ describe 'Band Search Model' do end it "filters by genre" do + pending band_id = band.id filter[BandSearch::KEY_GENRES] = [band_id] search.search_results_page(BandSearch::TO_JOIN, filter) diff --git a/ruby/spec/jam_ruby/models/download_tracker_spec.rb b/ruby/spec/jam_ruby/models/download_tracker_spec.rb new file mode 100644 index 000000000..041abdf04 --- /dev/null +++ b/ruby/spec/jam_ruby/models/download_tracker_spec.rb @@ -0,0 +1,162 @@ +require 'spec_helper' + +describe DownloadTracker do + + let(:user1) {FactoryGirl.create(:user)} + let(:user2) {FactoryGirl.create(:user)} + let(:user3) {FactoryGirl.create(:user)} + let(:jam_track) {FactoryGirl.create(:jam_track)} + + describe "check_user_sharer" do + describe "with max 1" do + it "and there is one row that is not paid for" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_user_sharer(1) + results.all.length.should eq 1 + results[0]['user_id'].should eql (user1.id) + end + end + + describe "with max 2" do + + it "and there is one row that is not paid for" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_user_sharer(2) + results.all.length.should eq 0 + + # and then add that same user at different IP, and see that something shows up + + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2') + results = DownloadTracker.check_user_sharer(2) + results.all.length.should eq 1 + end + + it "and there are two rows from different IP address, different jam tracks" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1') + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2') + + results = DownloadTracker.check_user_sharer(2) + results.all.length.should eq 1 + results[0]['user_id'].should eql(user1.id) + results[0]['count'].should eql 2 + + # now add a second user with one of the previous IP addresses; shouldn't matter yet + tracker1 = FactoryGirl.create(:download_tracker, user: user2, paid: false, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_user_sharer(2) + results.all.length.should eq 1 + results[0]['user_id'].should eql(user1.id) + results[0]['count'].should eql 2 + end + + it "and there are two rows from same IP adresss, same jam track" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1', jam_track: jam_track) + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1', jam_track: jam_track) + + results = DownloadTracker.check_user_sharer(2) + results.all.length.should eq 0 + + tracker1 = FactoryGirl.create(:download_tracker, user: user2, paid: false, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_user_sharer(2) + results.all.length.should eq 0 + + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2') + + results = DownloadTracker.check_user_sharer(2) + results.all.length.should eq 1 + results[0]['user_id'].should eql(user1.id) + results[0]['count'].should eql 3 + end + + it "and there are two rows from same user one paid for, one not" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: true, remote_ip: '1.1.1.1') + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2') + + results = DownloadTracker.check_user_sharer(2) + results.all.length.should eq 1 + end + end + end + + describe "check_freebie_snarfer" do + + describe "with max 1" do + + it "and there is one row that is not paid for" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_freebie_snarfer(1) + results.all.length.should eq 1 + end + + it "and there is one row that is paid for" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: true, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_freebie_snarfer(1) + results.all.length.should eq 0 + end + end + describe "with max 2" do + + it "and there is one row that is not paid for" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_freebie_snarfer(2) + results.all.length.should eq 0 + + # and then add a second user at same IP, and see that something shows up + + tracker1 = FactoryGirl.create(:download_tracker, user: user2, paid: false, remote_ip: '1.1.1.1') + results = DownloadTracker.check_freebie_snarfer(2) + results.all.length.should eq 1 + end + + it "and there are two rows from same user, different jam tracks" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1') + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_freebie_snarfer(2) + results.all.length.should eq 1 + results[0]['remote_ip'].should eql('1.1.1.1') + results[0]['count'].should eql 2 + + tracker1 = FactoryGirl.create(:download_tracker, user: user2, paid: false, remote_ip: '2.2.2.2') + + results = DownloadTracker.check_freebie_snarfer(2) + results.all.length.should eq 1 + results[0]['remote_ip'].should eql('1.1.1.1') + results[0]['count'].should eql 2 + end + + it "and there are two rows from same user, same jam track" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1', jam_track: jam_track) + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1', jam_track: jam_track) + + results = DownloadTracker.check_freebie_snarfer(2) + results.all.length.should eq 0 + + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2') + + results = DownloadTracker.check_freebie_snarfer(2) + results.all.length.should eq 0 + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_freebie_snarfer(2) + results.all.length.should eq 1 + results[0]['remote_ip'].should eql('1.1.1.1') + results[0]['count'].should eql 3 + end + + it "and there are two rows from same user one paid for, one not" do + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: true, remote_ip: '1.1.1.1') + tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1') + + results = DownloadTracker.check_freebie_snarfer(2) + results.all.length.should eq 0 + end + end + end +end diff --git a/ruby/spec/jam_ruby/models/email_blacklist_spec.rb b/ruby/spec/jam_ruby/models/email_blacklist_spec.rb new file mode 100644 index 000000000..35c4097df --- /dev/null +++ b/ruby/spec/jam_ruby/models/email_blacklist_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe EmailBlacklist do + + let(:user) { FactoryGirl.create(:user) } + + describe "#banned" do + it "returns false if no ban" do + EmailBlacklist.banned(user).should eq false + end + + it "returns true if banned" do + FactoryGirl.create(:email_blacklist, email: user.email) + EmailBlacklist.banned(user).should eq true + end + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/ip_blacklist_spec.rb b/ruby/spec/jam_ruby/models/ip_blacklist_spec.rb new file mode 100644 index 000000000..4f0416c30 --- /dev/null +++ b/ruby/spec/jam_ruby/models/ip_blacklist_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe IpBlacklist do + + describe "#banned" do + it "returns false if no ban" do + IpBlacklist.banned('1.1.1.1').should eq false + end + + it "returns true if banned" do + FactoryGirl.create(:ip_blacklist, remote_ip: "1.1.1.1") + IpBlacklist.banned('1.1.1.1').should eq true + end + + it "returns false if whitelisted" do + FactoryGirl.create(:ip_whitelist, remote_ip: "1.1.1.1") + IpBlacklist.banned('1.1.1.1').should eq false + end + + + it "returns false if whitelisted and blacklisted too" do + FactoryGirl.create(:ip_blacklist, remote_ip: "1.1.1.1") + FactoryGirl.create(:ip_whitelist, remote_ip: "1.1.1.1") + IpBlacklist.banned('1.1.1.1').should eq false + end + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/jam_class_report_spec.rb b/ruby/spec/jam_ruby/models/jam_class_report_spec.rb new file mode 100644 index 000000000..2643360a7 --- /dev/null +++ b/ruby/spec/jam_ruby/models/jam_class_report_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe JamClassReport do + + it "wee bit of data" do + user = FactoryGirl.create(:user, origin_utm_campaign: 'legacy') + + query = JamClassReport.analyse + query.length.should eql 2 + r1 = query[0] + + r1.cohort.should eql Date.new(user.created_at.year, user.created_at.month, 1) + r1.registrations.should eql 1 + r1.campaign.should eql 'legacy' + r1.spend.should be_nil + r1.registrations.should eql 1 + r1.td_customers.should eql 0 + r1.jamclass_rev.should be_nil + r1.td4.should eql 0 + r1.td2.should eql 0 + r1.td1.should eql 0 + r1.spend_td.should be_nil + r1.purchases0.should eql 1 + r1.purchases1.should eql 0 + r1.purchases2.should eql 0 + r1.purchases3.should eql 0 + r1.purchases_rest.should eql 0 + + r2 = query[1] + r2.cohort.should be_nil + r2.registrations.should eql 1 + r2.campaign.should eql 'legacy' + r2.spend.should be_nil + r2.registrations.should eql 1 + r2.td_customers.should eql 0 + r2.jamclass_rev.should be_nil + r2.td4.should eql 0 + r2.td2.should eql 0 + r2.td1.should eql 0 + r2.spend_td.should be_nil + r2.purchases0.should eql 1 + r2.purchases1.should eql 0 + r2.purchases2.should eql 0 + r2.purchases3.should eql 0 + r2.purchases_rest.should eql 0 + + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb b/ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb new file mode 100644 index 000000000..3a90ffcc3 --- /dev/null +++ b/ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb @@ -0,0 +1,103 @@ +require 'spec_helper' + +describe JamTrackMixdownPackage do + include UsesTempFiles + + it "can be created (factory girl)" do + package = FactoryGirl.create(:jam_track_mixdown_package) + end + + it "can be created" do + mixdown= FactoryGirl.create(:jam_track_mixdown) + + package = JamTrackMixdownPackage.create(mixdown, JamTrackMixdownPackage::FILE_TYPE_OGG, 48, 'jkz') + + package.errors.any?.should == false + end + + + describe "signing_state" do + it "quiet" do + package = FactoryGirl.create(:jam_track_mixdown_package) + package.signing_state.should eq('QUIET') + end + + it "signed" do + package = FactoryGirl.create(:jam_track_mixdown_package, signed: true, signing_started_at: Time.now) + package.signing_state.should eq('SIGNED') + end + + it "error" do + package = FactoryGirl.create(:jam_track_mixdown_package, error_count: 1) + package.signing_state.should eq('ERROR') + end + + it "signing" do + package = FactoryGirl.create(:jam_track_mixdown_package, signing:true, signing_started_at: Time.now, packaging_steps: 3, current_packaging_step:0, last_step_at:Time.now) + package.signing_state.should eq('SIGNING') + end + + it "signing timeout" do + package = FactoryGirl.create(:jam_track_mixdown_package, signing: true, signing_started_at: Time.now - (APP_CONFIG.signing_job_signing_max_time + 1), packaging_steps: 3, current_packaging_step:0, last_step_at:Time.now) + package.signing_state.should eq('SIGNING_TIMEOUT') + end + + it "queued" do + package = FactoryGirl.create(:jam_track_mixdown_package, signing_queued_at: Time.now) + package.signing_state.should eq('QUEUED') + end + + it "signing timeout" do + package = FactoryGirl.create(:jam_track_mixdown_package, signing_queued_at: Time.now - (APP_CONFIG.mixdown_job_queue_max_time + 1)) + package.signing_state.should eq('QUEUED_TIMEOUT') + end + end + + describe "stats" do + + it "empty" do + JamTrackMixdownPackage.stats['count'].should eq(0) + end + + it "signing" do + package = FactoryGirl.create(:jam_track_mixdown_package) + JamTrackMixdownPackage.stats.should eq('count' => 1, + 'signing_count' => 0) + + package.signing = true + package.save! + + JamTrackMixdownPackage.stats.should eq('count' => 1, + 'signing_count' => 1) + end + end + + describe "estimated_queue_time" do + it "succeeds with no data" do + JamTrackMixdownPackage.estimated_queue_time.should eq(0) + end + + it "mixdown packages of different sorts" do + package = FactoryGirl.create(:jam_track_mixdown_package, speed_pitched: true) + JamTrackMixdownPackage.estimated_queue_time.should eq(0) + + package.queued = true + package.save! + JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_slow_mixdown_time * 1) + + package.speed_pitched = false + package.save! + + JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_fast_mixdown_time * 1) + + right = FactoryGirl.create(:jam_track_right) + JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_fast_mixdown_time * 1) + + right.queued = true + right.save! + JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_fast_mixdown_time * 1 + APP_CONFIG.estimated_jam_track_time * 1) + end + + end +end + diff --git a/ruby/spec/jam_ruby/models/jam_track_mixdown_spec.rb b/ruby/spec/jam_ruby/models/jam_track_mixdown_spec.rb new file mode 100644 index 000000000..fb42ed208 --- /dev/null +++ b/ruby/spec/jam_ruby/models/jam_track_mixdown_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe JamTrackMixdown do + + let(:user) {FactoryGirl.create(:user)} + let(:jam_track) {FactoryGirl.create(:jam_track)} + let(:settings) { {speed:5} } + + it "can be created (factory girl)" do + mixdown = FactoryGirl.create(:jam_track_mixdown) + + mixdown = JamTrackMixdown.find(mixdown.id) + mixdown.settings.should eq({"speed" => 5}) + end + + it "can be created" do + mixdown = JamTrackMixdown.create('abc', 'description', user, jam_track, settings) + mixdown.errors.any?.should == false + end + + it "index" do + query, start, count = JamTrackMixdown.index({id: jam_track}, user) + + query.length.should eq(0) + start.should be_nil + count.should eq(0) + + mixdown = FactoryGirl.create(:jam_track_mixdown, user: user, jam_track: jam_track) + + query, start, count = JamTrackMixdown.index({id: jam_track}, user) + query[0].should eq(mixdown) + start.should be_nil + count.should eq(1) + end + + describe "settings" do + it "validates empty settings" do + invalid = FactoryGirl.build(:jam_track_mixdown, settings: {}.to_json) + invalid.save + invalid.errors.any?.should be_true + invalid.errors["settings"].should eq(["can't be blank", "have nothing specified"]) + end + + it "validates speed numeric" do + invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"speed" => "5"}.to_json) + invalid.save + invalid.errors.any?.should be_true + invalid.errors["settings"].should eq(["has non-integer speed"]) + end + + it "validates pitch numeric" do + invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"pitch" => "5"}.to_json) + invalid.save + invalid.errors.any?.should be_true + invalid.errors["settings"].should eq(["has non-integer pitch"]) + end + + it "validates speed not-float" do + invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"speed" => 5.5}.to_json) + invalid.save + invalid.errors.any?.should be_true + invalid.errors["settings"].should eq(["has non-integer speed"]) + end + + it "validates pitch not-float" do + invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"pitch" => 10.5}.to_json) + invalid.save + invalid.errors.any?.should be_true + invalid.errors["settings"].should eq(["has non-integer pitch"]) + end + end + + +end + diff --git a/ruby/spec/jam_ruby/models/jam_track_right_spec.rb b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb index 9119bdfd9..9e7bc225e 100644 --- a/ruby/spec/jam_ruby/models/jam_track_right_spec.rb +++ b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb @@ -29,6 +29,15 @@ describe JamTrackRight do end end + describe "private keys automatically created" do + it "created automatically" do + jam_track_right = FactoryGirl.create(:jam_track_right) + jam_track_right.private_key_44.should_not be_nil + jam_track_right.private_key_48.should_not be_nil + jam_track_right.private_key_44.should eq(jam_track_right.private_key_48) + end + end + describe "JKZ" do before(:all) do original_storage = JamTrackTrackUploader.storage = :fog @@ -109,12 +118,14 @@ 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_44: 'keyabc') + jam_track_right = FactoryGirl.create(:jam_track_right) 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_44'].should eq('keyabc') - keys[0]['private_key_48'].should be_nil + keys[0]['private_key_44'].should_not be_nil + keys[0]['private_key_48'].should_not be_nil + keys[0]['private_key_44'].should eq(jam_track_right.private_key_44) + keys[0]['private_key_48'].should eq(jam_track_right.private_key_48) end end diff --git a/ruby/spec/jam_ruby/models/jam_track_search_spec.rb b/ruby/spec/jam_ruby/models/jam_track_search_spec.rb new file mode 100644 index 000000000..d4a3dc93d --- /dev/null +++ b/ruby/spec/jam_ruby/models/jam_track_search_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe 'JamTrack Search Model' do + + let(:artist_filter) { + filter = JamTrackSearch.json_schema.clone + filter[JamTrackSearch::KEY_RESULT_TYPES] = [JamTrackSearch::KEY_ARTISTS] + filter + } + let(:song_filter) { + filter = JamTrackSearch.json_schema.clone + filter[JamTrackSearch::KEY_RESULT_TYPES] = [JamTrackSearch::KEY_SONGS] + filter + } + let(:freebird) { + FactoryGirl.create(:jam_track_with_tracks, original_artist: 'jim bob', name: 'freebird') + } + let(:stairway) { + FactoryGirl.create(:jam_track_with_tracks, original_artist: 'jim bob', name: 'stairway to heaven') + } + + before :each do + JamTrack.delete_all + JamTrackTrack.delete_all + freebird + stairway + end + + describe "Search filter" do + + it "finds by artist" do + pending + filter = artist_filter.clone + filter[JamTrackSearch::KEY_SEARCH_STR] = freebird.original_artist + filter = JamTrackSearch.new.search_results_page(filter['query']) + expect(filter[JamTrackSearch::KEY_RESULTS][JamTrackSearch::KEY_ARTISTS].count).to be(1) + end + + it "paginates by artist" do + pending + JamTrackSearch::PER_PAGE.times do |nn| + FactoryGirl.create(:jam_track_with_tracks, + original_artist: freebird.original_artist + nn.to_s, + name: 'abc'+nn.to_s) + end + filter = artist_filter.clone + filter[JamTrackSearch::KEY_SEARCH_STR] = freebird.original_artist + out_filter = JamTrackSearch.new.search_results_page(filter.clone['query']) + expect(out_filter[JamTrackSearch::KEY_RESULTS][JamTrackSearch::KEY_ARTISTS].count).to be([JamTrackSearch::PER_PAGE, JamTrack.count].min) + num_page = (JamTrack.count / JamTrackSearch::PER_PAGE) + 1 + expect(out_filter[JamTrackSearch::KEY_ARTISTS]['page_count']).to be(num_page) + + filter[JamTrackSearch::KEY_ARTISTS]['page_num'] = 2 + out_filter = JamTrackSearch.new.search_results_page(filter.clone['query']) + expect(out_filter[JamTrackSearch::KEY_RESULTS][JamTrackSearch::KEY_ARTISTS].count).to be(1) + end + + it "finds by song" do + pending + filter = song_filter.clone + filter[JamTrackSearch::KEY_SEARCH_STR] = freebird.name + filter = JamTrackSearch.new.search_results_page(filter.clone['query']) + expect(filter[JamTrackSearch::KEY_RESULTS][JamTrackSearch::KEY_SONGS].count).to be(1) + end + + it "paginates by song" do + pending + (JamTrackSearch::PER_PAGE + 2).times do |nn| + FactoryGirl.create(:jam_track_with_tracks, + original_artist: freebird.original_artist, + name: 'abc'+nn.to_s) + end + filter = song_filter.clone + filter[JamTrackSearch::KEY_SEARCH_STR] = 'abc' + out_filter = JamTrackSearch.new.search_results_page(filter.clone['query']) + expect(out_filter[JamTrackSearch::KEY_RESULTS][JamTrackSearch::KEY_SONGS].count).to be([JamTrackSearch::PER_PAGE, JamTrack.count].min) + + total_count = JamTrack.where("name LIKE 'abc%'").count + num_page = (total_count / JamTrackSearch::PER_PAGE) + (0==(total_count % JamTrackSearch::PER_PAGE) ? 0 : 1) + expect(out_filter[JamTrackSearch::KEY_SONGS]['page_count']).to be(num_page) + + filter[JamTrackSearch::KEY_SONGS]['page_num'] = 2 + out_filter = JamTrackSearch.new.search_results_page(filter.clone['query']) + expect(out_filter[JamTrackSearch::KEY_RESULTS][JamTrackSearch::KEY_SONGS].count).to be(2) + end + + end + +end diff --git a/ruby/spec/jam_ruby/models/jam_track_spec.rb b/ruby/spec/jam_ruby/models/jam_track_spec.rb index 6c54a9160..022c5c15b 100644 --- a/ruby/spec/jam_ruby/models/jam_track_spec.rb +++ b/ruby/spec/jam_ruby/models/jam_track_spec.rb @@ -234,6 +234,17 @@ describe JamTrack do query.size.should == 2 end + + it "deals with aggregration (regression)" do + + query, pager, count = JamTrack.index({sort_by: 'jamtrack', artist: 'K.C. And The Sunshine Band'}, user) + count.should == 0 + + jam_track1 = FactoryGirl.create(:jam_track_with_tracks, name: 'Take a Chance On Me', original_artist: 'K.C. And The Sunshine Band') + + query, pager, count = JamTrack.index({sort_by: 'jamtrack', artist: 'K.C. And The Sunshine Band'}, user) + count.should == 1 + end end describe "validations" do diff --git a/ruby/spec/jam_ruby/models/jamblaster_spec.rb b/ruby/spec/jam_ruby/models/jamblaster_spec.rb new file mode 100644 index 000000000..6339951b2 --- /dev/null +++ b/ruby/spec/jam_ruby/models/jamblaster_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Jamblaster do + + let(:jamblaster) { FactoryGirl.create(:jamblaster) } + let(:user) { FactoryGirl.create(:user) } + + it "can be created" do + FactoryGirl.create(:jamblaster) + end + + it "can associate to users" do + jamblaster.users.should eq([]) + user.jamblasters.should eq([]) + end + + describe "most_recent_pairings" do + it "basic funnction" do + + jamblaster.most_recent_pairing.should be nil + + pairing1 = FactoryGirl.create(:jamblaster_pairing_request, user: user, jamblaster: jamblaster, vtoken: 'token2', sibling_key: nil) + + jamblaster.most_recent_pairing.should be nil + + pairing1.activate('abc') + + jamblaster.most_recent_pairing.should eql pairing1 + + pairing2 = FactoryGirl.create(:jamblaster_pairing_request, user: user, jamblaster: jamblaster, sibling_key: 'key1', vtoken: 'token1') + + jamblaster.most_recent_pairing.should eql pairing1 + + pairing2.activate('key2') + + jamblaster.most_recent_pairing.should eql pairing2 + end + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/language_spec.rb b/ruby/spec/jam_ruby/models/language_spec.rb new file mode 100644 index 000000000..ae1961cf1 --- /dev/null +++ b/ruby/spec/jam_ruby/models/language_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe Language do + + it "english_sort" do + sorted= Language.english_sort + sorted[0].id.should eql 'EN' + sorted[1].id.should eql 'AF' + sorted[-1].id.should eql 'XH' + end +end diff --git a/ruby/spec/jam_ruby/models/lesson_booking_slot_spec.rb b/ruby/spec/jam_ruby/models/lesson_booking_slot_spec.rb new file mode 100644 index 000000000..fb1019b97 --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_booking_slot_spec.rb @@ -0,0 +1,172 @@ +require 'spec_helper' + +# collissions with teacher's schedule? +describe LessonBookingSlot do + + let(:user) { FactoryGirl.create(:user, stored_credit_card: false, remaining_free_lessons: 1, remaining_test_drives: 1) } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single, timezone: 'US/Pacific', hour: 12) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring, timezone: 'US/Pacific', hour: 12) } + + + before do + + end + + after do + Timecop.return + end + + describe "next_day" do + before do + # Set Time.now to September 1, 2008 10:05:00 AM (at this instant). This is Monday + @now = Time.local(2008, 9, 1, 10, 5, 0) + Timecop.freeze(@now) + end + it "same day of week" do + + @now.wday.should eql 1 + # let's make it match + lesson_booking_slot_recurring1.day_of_week = @now.wday + + (@now.to_date).should eql lesson_booking_slot_recurring1.next_day + end + + it "day of week is tomorrow" do + + # make the slot be today + 1 (Tuesday in this case) + lesson_booking_slot_recurring1.day_of_week = @now.wday + 1 + + lesson_booking_slot_recurring1.next_day.should eql(@now.to_date + 1) + end + + it "day of week is yesterday" do + # make the slot be today + 1 (Sunday in this case) + lesson_booking_slot_recurring1.day_of_week = @now.wday - 1 + + lesson_booking_slot_recurring1.next_day.should eql(@now.to_date + 6) + end + + it "day of week is two day ago" do + # make the slot be today + 1 (Saturday in this case) + lesson_booking_slot_recurring1.day_of_week = 6 + + lesson_booking_slot_recurring1.next_day.should eql(@now.to_date + 5) + end + end + + describe "scheduled_time" do + before do + # Set Time.now to September 1, 2008 10:05:00 AM (at this instant). This is Monday + @now = Time.local(2008, 9, 1, 10, 5, 0) + Timecop.freeze(@now) + end + + it "creates a single time OK" do + time = lesson_booking_slot_single1.scheduled_time(0) + time.utc_offset.should eq 0 + time.year.should eql lesson_booking_slot_single1.preferred_day.year + time.month.should eql lesson_booking_slot_single1.preferred_day.month + time.day.should eql lesson_booking_slot_single1.preferred_day.day + time.hour.should be > lesson_booking_slot_single1.hour + time.min.should eql lesson_booking_slot_single1.minute + time.sec.should eql 0 + end + + it "creates a recurring time OK" do + lesson_booking_slot_recurring1.day_of_week = @now.wday + time = lesson_booking_slot_recurring1.scheduled_time(0) + time.utc_offset.should eq 0 + time.year.should eql @now.year + time.month.should eql @now.month + time.day.should eql @now.day + time.hour.should be > lesson_booking_slot_recurring1.hour + time.min.should eql lesson_booking_slot_recurring1.minute + time.sec.should eql 0 + end + + it "creates a recurring time OK 1 week out" do + lesson_booking_slot_recurring1.day_of_week = @now.wday + time = lesson_booking_slot_recurring1.scheduled_time(1) + time.utc_offset.should eq 0 + time.year.should eql @now.year + time.month.should eql @now.month + time.day.should eql @now.day + 7 + time.hour.should be > lesson_booking_slot_recurring1.hour + time.min.should eql lesson_booking_slot_recurring1.minute + time.sec.should eql 0 + end + end + + + describe "scheduled_times" do + before do + # Set Time.now to September 1, 2008 10:05:00 AM (at this instant). This is Monday + @now = Time.local(2008, 9, 1, 10, 5, 0) + Timecop.freeze(@now) + end + + it "creates a single time session OK" do + times = lesson_booking_slot_single1.scheduled_times(1, @now.to_date + 2) + times.length.should eq 1 + time = times[0] + time.utc_offset.should eq 0 + time.year.should eql lesson_booking_slot_single1.preferred_day.year + time.month.should eql lesson_booking_slot_single1.preferred_day.month + time.day.should eql lesson_booking_slot_single1.preferred_day.day + time.hour.should be > lesson_booking_slot_single1.hour + time.min.should eql lesson_booking_slot_single1.minute + time.sec.should eql 0 + end + + + it "creates a recurring time session OK" do + lesson_booking_slot_recurring1.day_of_week = @now.wday + 3 + times = lesson_booking_slot_recurring1.scheduled_times(1, @now.to_date + 2) + times.length.should eq 1 + time = times[0] + time.utc_offset.should eq 0 + time.year.should eql @now.year + time.month.should eql @now.month + time.day.should eql @now.day + 3 + time.hour.should be > lesson_booking_slot_recurring1.hour + time.min.should eql lesson_booking_slot_recurring1.minute + time.sec.should eql 0 + end + + it "creates a recurring time session OK (minimum time hit)" do + lesson_booking_slot_recurring1.day_of_week = @now.wday + times = lesson_booking_slot_recurring1.scheduled_times(1, @now.to_date + 2) + times.length.should eq 1 + time = times[0] + time.utc_offset.should eq 0 + time.year.should eql @now.year + time.month.should eql @now.month + time.day.should eql @now.day + 7 + time.hour.should be > lesson_booking_slot_recurring1.hour + time.min.should eql lesson_booking_slot_recurring1.minute + time.sec.should eql 0 + end + + it "creates 2 recurring times session OK" do + lesson_booking_slot_recurring1.day_of_week = @now.wday + times = lesson_booking_slot_recurring1.scheduled_times(2, @now.to_date + 2) + times.length.should eq 2 + time = times[0] + time.utc_offset.should eq 0 + time.year.should eql @now.year + time.month.should eql @now.month + time.day.should eql @now.day + 7 + time.hour.should be > lesson_booking_slot_recurring1.hour + time.min.should eql lesson_booking_slot_recurring1.minute + time.sec.should eql 0 + time = times[1] + time.utc_offset.should eq 0 + time.year.should eql @now.year + time.month.should eql @now.month + time.day.should eql @now.day + 14 + time.hour.should be > lesson_booking_slot_recurring1.hour + time.min.should eql lesson_booking_slot_recurring1.minute + time.sec.should eql 0 + end + end +end diff --git a/ruby/spec/jam_ruby/models/lesson_booking_spec.rb b/ruby/spec/jam_ruby/models/lesson_booking_spec.rb new file mode 100644 index 000000000..47b3cd0b8 --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_booking_spec.rb @@ -0,0 +1,884 @@ +require 'spec_helper' + +# collissions with teacher's schedule? +describe LessonBooking do + + let(:user) { FactoryGirl.create(:user, stored_credit_card: false, remaining_free_lessons: 1, remaining_test_drives: 1) } + let(:teacher_user) { FactoryGirl.create(:teacher_user) } + let(:teacher) { teacher_user.teacher } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] } + let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } + + describe "suspend!" do + it "should set status as well as update status of all associated lesson_sessions" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.lesson_sessions[0].accept({accepter: teacher_user, message: "got it", slot: booking.lesson_booking_slots[0].id}) + booking.reload + booking.active.should eql true + booking.suspend! + booking.errors.any?.should be false + booking.reload + booking.active.should eql false + booking.status.should eql LessonBooking::STATUS_SUSPENDED + booking.lesson_sessions.count.should eql 2 + booking.lesson_sessions.each do |lesson_session| + lesson_session.status.should eql LessonBooking::STATUS_SUSPENDED + end + end + end + + describe "bill_monthlies" do + after do + Timecop.return + end + + it "empty" do + LessonBooking.count.should eql 0 + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 0 + end + + it "one" do + day = Date.new(2016, 1, 1) + time = day.to_time + Timecop.freeze(time) + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.accept(booking.lesson_sessions[0], booking.lesson_booking_slots[0], teacher_user) + booking.errors.any?.should be false + + LessonBooking.count.should eql 1 + LessonPackagePurchase.count.should eql 0 + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + + purchase = LessonPackagePurchase.first + purchase.billing_error_reason.should eql "stripe" + purchase.billing_error_detail.should include("Cannot charge a customer that has no active card") + purchase.month.should eql 1 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql false + purchase.billed_at.should be_nil + purchase.billing_attempts.should eql 1 + purchase.post_processed.should eql false + purchase.post_processed_at.should be_nil + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql false + + # don't advance time, but nothing should happen because last_billing time hasn't elapsed + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + purchase.billing_error_reason.should eql "stripe" + purchase.billing_error_detail.should include("Cannot charge a customer that has no active card") + purchase.month.should eql 1 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql false + purchase.billed_at.should eql nil + purchase.billing_attempts.should eql 1 + purchase.post_processed.should eql false + purchase.post_processed_at.should be_nil + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql false + + day = day + 1 + time = day.to_time + Timecop.freeze(time) + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + + purchase.billing_error_reason.should eql "stripe" + purchase.billing_error_detail.should include("Cannot charge a customer that has no active card") + purchase.month.should eql 1 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql false + purchase.billed_at.should eql nil + purchase.billing_attempts.should eql 2 + purchase.post_processed.should eql false + purchase.post_processed_at.should be_nil + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql false + + user.card_approved(create_stripe_token, '78759', booking.id) + user.save! + + day = day + 1 + time = day.to_time + Timecop.freeze(time) + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + purchase.month.should eql 1 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql true + purchase.billed_at.should eql time + # purchase.billing_error_reason.should be nil + purchase.billing_attempts.should eql 3 + purchase.post_processed.should eql true + purchase.post_processed_at.should eql time + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql true + end + + it "advances to next month" do + user.card_approved(create_stripe_token, '78759', nil) + user.save! + + day = Date.new(2016, 1, 20) + time = day.to_time + Timecop.freeze(time) + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.accept(booking.lesson_sessions[0], booking.lesson_booking_slots[0], teacher_user) + booking.errors.any?.should be false + + LessonBooking.count.should eql 1 + LessonPackagePurchase.count.should eql 0 + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + LessonSession.count.should eql 2 + + purchase = LessonPackagePurchase.first + purchase.month.should eql 1 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql true + purchase.billed_at.should eql time + # purchase.billing_error_reason.should be nil + purchase.billing_attempts.should eql 1 + purchase.post_processed.should eql true + purchase.post_processed_at.should eql time + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql true + + day = Date.new(2016, 1, 27) + time = day.to_time + Timecop.freeze(time) + + LessonBooking.schedule_upcoming_lessons + LessonSession.count.should eql 3 + LessonPackagePurchase.count.should eql 1 + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 2 + + purchase = LessonPackagePurchase.order(:month).last + purchase.month.should eql 2 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql true + purchase.billed_at.should eql time + # purchase.billing_error_reason.should be nil + purchase.billing_attempts.should eql 1 + purchase.post_processed.should eql true + purchase.post_processed_at.should eql time + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql true + end + + it "will suspend after enough tries" do + day = Date.new(2016, 1, 1) + time = day.to_time + Timecop.freeze(time) + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.accept(booking.lesson_sessions[0], booking.lesson_booking_slots[0], teacher_user) + booking.errors.any?.should be false + + LessonBooking.count.should eql 1 + LessonPackagePurchase.count.should eql 0 + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + + purchase = LessonPackagePurchase.first + purchase.billing_error_reason.should eql "stripe" + purchase.billing_error_detail.should include("Cannot charge a customer that has no active card") + purchase.billing_attempts.should eql 1 + booking.is_suspended?.should be_false + purchase.billed.should be false + day = day + 1 + time = day.to_time + Timecop.freeze(time) + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + purchase.billing_attempts.should eql 2 + booking.reload + booking.is_suspended?.should be_false + purchase.billed.should be false + + day = day + 1 + time = day.to_time + Timecop.freeze(time) + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + purchase.billing_attempts.should eql 3 + booking.reload + booking.is_suspended?.should be_false + purchase.billed.should be false + + day = day + 1 + time = day.to_time + Timecop.freeze(time) + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + purchase.billing_attempts.should eql 4 + booking.reload + booking.is_suspended?.should be_false + purchase.billed.should be false + + day = day + 1 + time = day.to_time + Timecop.freeze(time) + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + purchase.billing_attempts.should eql 5 + booking.reload + booking.is_suspended?.should be_true + purchase.billed.should be false + + # now that it's suspended, let's unsuspend it + user.card_approved(create_stripe_token, '78759', booking.id) + user.save! + + day = day + 1 + time = day.to_time + Timecop.freeze(time) + purchase.bill_monthly(true) + LessonPackagePurchase.count.should eql 1 + purchase.reload + + purchase.month.should eql 1 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql true + purchase.billed_at.should eql time + # purchase.billing_error_reason.should be nil + purchase.billing_attempts.should eql 6 + purchase.post_processed.should eql true + purchase.post_processed_at.should eql time + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql true + booking.reload + booking.is_suspended?.should be_false + end + + it "missed meetings deduct on next month" do + # TODO. Discuss with David a little more + end + end + + describe "billable_monthlies" do + after do + Timecop.return + end + + it "empty" do + LessonBooking.billable_monthlies(Time.now).count.should eql 0 + end + + it "one" do + time = Date.new(2016, 1, 1) + Timecop.freeze(time) + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.accept(booking.lesson_sessions[0], booking.lesson_booking_slots[0], teacher_user) + booking.errors.any?.should be false + + now = Time.now + billables = LessonBooking.billable_monthlies(now) + billables.all.to_a.should eql [booking] + LessonPackagePurchase.where(lesson_booking_id: booking.id).count.should eql 0 + # to make this billable monthly go away, we will need to create one LessonPackagePurchase; one for this month (because it's only one we have lessons in) + # and further, mark them both as post_processed + + package = LessonPackagePurchase.create(user, booking, LessonPackageType.single, 2016, 1) + + LessonBooking.billable_monthlies(now).count.should eql 1 + + package.post_processed = true + package.save! + + LessonBooking.billable_monthlies(now).count.should eql 0 + end + end + + describe "predicted_times_for_month" do + after do + Timecop.return + end + it "fills up month" do + + next_year = Time.now.year + 1 + jan1 = Date.new(next_year, 1, 1) + jan31 = Date.new(next_year, 1, -1) + Timecop.freeze(jan1) + slot = valid_recurring_slots[0] + slot.day_of_week = jan1.wday + + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + times = booking.predicted_times_for_month(next_year, 1) + times.length.should eql 5 + times[0].to_date.should eql (jan1) + times[1].to_date.should eql (Date.new(next_year, 1, 8)) + times[2].to_date.should eql (Date.new(next_year, 1, 15)) + times[3].to_date.should eql (Date.new(next_year, 1, 22)) + times[4].to_date.should eql (Date.new(next_year, 1, 29)) + end + + it "fills up partial month" do + next_year = Time.now.year + 1 + jan1 = Date.new(next_year, 1, 1) + jan15 = Date.new(next_year, 1, 15) + jan31 = Date.new(next_year, 1, -1) + Timecop.freeze(jan15) + slot = valid_recurring_slots[0] + slot.day_of_week = jan1.wday + + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + times = booking.predicted_times_for_month(next_year, 1) + times.length.should eql 3 + times[0].to_date.should eql (Date.new(next_year, 1, 15)) + times[1].to_date.should eql (Date.new(next_year, 1, 22)) + times[2].to_date.should eql (Date.new(next_year, 1, 29)) + end + + it "let's assume JamKazam is messed up for a few weeks" do + next_year = Time.now.year + 1 + jan1 = Date.new(next_year, 1, 1) + jan15 = Date.new(next_year, 1, 15) + jan31 = Date.new(next_year, 1, -1) + Timecop.freeze(jan1) + slot = valid_recurring_slots[0] + slot.day_of_week = jan1.wday + + # book a session on jan1 + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + + # but don't run the computation of times per month for weeks + Timecop.freeze(Date.new(next_year, 1, 23)) + times = booking.predicted_times_for_month(next_year, 1) + times[:times].length.should eql 2 + times[:times][0].to_date.should eql (Date.new(next_year, 1, 1)) + times[:times][1].to_date.should eql (Date.new(next_year, 1, 29)) + end + end + + describe "book_free" do + + it "allows long message to flow through chat" do + + user.has_free_lessons?.should be_true + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, Faker::Lorem.characters(10000)) + + booking.errors.any?.should be false + + chat_message = ChatMessage.where(lesson_session_id: booking.next_lesson.id).first + chat_message.should_not be_nil + chat_message.message.should eq booking.description + end + + it "prevents user without free lesson" do + + pending "free not supported" + booking = LessonBooking.book_free(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be false + + ChatMessage.count.should eq 1 + + booking = LessonBooking.book_free(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be true + booking.errors[:user].should eq ["have no remaining free lessons"] + + ChatMessage.count.should eq 1 + end + + it "must have 2 lesson booking slots" do + + booking = LessonBooking.book_test_drive(user, teacher_user, [], "Hey I've heard of you before.") + booking.errors.any?.should be true + booking.errors[:lesson_booking_slots].should eq ["must have two times specified"] + end + + it "must have well-formed booking slots" do + bad_slot = FactoryGirl.build(:lesson_booking_slot_single, minute: nil) + booking = LessonBooking.book_free(user, teacher_user, [lesson_booking_slot_single1, bad_slot], "Hey I've heard of you before.") + booking.errors.any?.should be true + booking.errors[:lesson_booking_slots].should eq ["is invalid"] + bad_slot = booking.lesson_booking_slots[1] + bad_slot.errors[:minute].should eq ["is not a number"] + end + end + + describe "book_test_drive" do + it "works" do + user.stored_credit_card = true + user.save! + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be false + booking.user.should eq user + booking.teacher.should eq teacher_user + booking.description.should eq ("Hey I've heard of you before.") + booking.payment_style.should eq LessonBooking::PAYMENT_STYLE_ELSEWHERE + booking.recurring.should eq false + booking.lesson_length.should eq 30 + booking.lesson_type.should eq LessonBooking::LESSON_TYPE_TEST_DRIVE + booking.lesson_booking_slots.length.should eq 2 + + chat_message = ChatMessage.where(lesson_session_id: booking.next_lesson.id).first + chat_message.should_not be_nil + chat_message.message.should eq booking.description + + user.reload + user.remaining_free_lessons.should eq 1 + user.remaining_test_drives.should eq 0 + end + + it "allows long message to flow through chat" do + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, Faker::Lorem.characters(10000)) + + booking.errors.any?.should be false + + chat_message = ChatMessage.where(lesson_session_id: booking.next_lesson.id).first + chat_message.should_not be_nil + chat_message.message.should eq booking.description + end + + it "prevents user without remaining test drives" do + + user.stored_credit_card = true + user.save! + + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be false + ChatMessage.count.should eq 1 + user.reload + user.remaining_test_drives.should eql 0 + + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be true + booking.errors[:user].should eq ["have an in-progress or successful TestDrive with this teacher already"] + + ChatMessage.count.should eq 1 + end + + + it "must have well-formed booking slots" do + bad_slot = FactoryGirl.build(:lesson_booking_slot_single, minute: nil) + booking = LessonBooking.book_test_drive(user, teacher_user, [lesson_booking_slot_single1, bad_slot], "Hey I've heard of you before.") + booking.errors.any?.should be true + booking.errors[:lesson_booking_slots].should eq ["is invalid"] + bad_slot = booking.lesson_booking_slots[1] + bad_slot.errors[:minute].should eq ["is not a number"] + end + end + + describe "book_normal" do + it "works" do + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + booking.errors.any?.should be false + booking.user.should eq user + booking.teacher.should eq teacher_user + booking.description.should eq ("Hey I've heard of you before.") + booking.payment_style.should eq LessonBooking::PAYMENT_STYLE_SINGLE + booking.recurring.should eq false + booking.lesson_length.should eq 60 + booking.lesson_type.should eq LessonBooking::LESSON_TYPE_PAID + booking.lesson_booking_slots.length.should eq 2 + + chat_message = ChatMessage.where(lesson_session_id: booking.next_lesson.id).first + chat_message.should_not be_nil + chat_message.message.should eq booking.description + + user.reload + user.remaining_free_lessons.should eq 1 + user.remaining_test_drives.should eq 1 + end + + it "works with recurring slots" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + booking.errors.any?.should be false + booking.user.should eq user + booking.teacher.should eq teacher_user + booking.description.should eq ("Hey I've heard of you before.") + booking.payment_style.should eq LessonBooking::PAYMENT_STYLE_WEEKLY + booking.recurring.should eq true + booking.lesson_length.should eq 60 + booking.lesson_type.should eq LessonBooking::LESSON_TYPE_PAID + booking.lesson_booking_slots.length.should eq 2 + + chat_message = ChatMessage.where(lesson_session_id: booking.next_lesson.id).first + chat_message.should_not be_nil + chat_message.message.should eq booking.description + + user.reload + user.remaining_free_lessons.should eq 1 + user.remaining_test_drives.should eq 1 + end + + it "allows long message to flow through chat" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, Faker::Lorem.characters(10000), true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + + booking.errors.any?.should be false + + chat_message = ChatMessage.where(lesson_session_id: booking.next_lesson.id).first + chat_message.should_not be_nil + chat_message.message.should eq booking.description + end + + it "does not prevent user without remaining test drives" do + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be false + + ChatMessage.count.should eq 1 + + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + + booking.errors.any?.should be false + + ChatMessage.count.should eq 2 + end + + it "does not prevents user without free lesson" do + + booking = LessonBooking.book_free(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be false + + ChatMessage.count.should eq 1 + + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + booking.errors.any?.should be false + + ChatMessage.count.should eq 2 + end + + it "does not prevent user without a stored credit card" do + user.stored_credit_card = false + user.save! + + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + booking.errors.any?.should be false + booking.card_presumed_ok.should eq false + booking.sent_notices.should eq false + end + + + it "must have well-formed booking slots" do + bad_slot = FactoryGirl.build(:lesson_booking_slot_recurring, day_of_week: nil) + booking = LessonBooking.book_test_drive(user, teacher_user, [lesson_booking_slot_recurring1, bad_slot], "Hey I've heard of you before.") + booking.errors.any?.should be true + booking.errors[:lesson_booking_slots].should eq ["is invalid"] + bad_slot = booking.lesson_booking_slots[1] + bad_slot.errors[:day_of_week].should eq ["must be specified"] + end + end + + describe "find_bookings_needing_sessions" do + after do + Timecop.return + end + it "can detect missing lesson and schedules it 1 week out" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + booking.lesson_sessions.length.should eql 1 + + booking.accept(booking.lesson_sessions[0], booking.lesson_booking_slots[0], teacher_user) + booking.errors.any?.should be false + booking.reload + booking.lesson_sessions.length.should eql 2 + + lesson_session = booking.lesson_sessions[0] + Timecop.freeze(lesson_session.music_session.scheduled_start) + + # causes find_bookings_needing_sessions to re-run + booking.sync_lessons + booking.reload + booking.lesson_sessions.length.should eql 3 + + # check that all the times make sense + + lesson1 = booking.lesson_sessions[0] + lesson2 = booking.lesson_sessions[1] + lesson3 = booking.lesson_sessions[2] + lesson1.music_session.scheduled_start.to_i.should eql (lesson2.music_session.scheduled_start.to_i - (60 * 60 * 24 * 7)) + lesson1.music_session.scheduled_start.to_i.should eql (lesson3.music_session.scheduled_start.to_i - (60 * 60 * 24 * 14)) + + Timecop.freeze(lesson2.music_session.scheduled_start) + + # causes find_bookings_needing_sessions to re-run + booking.sync_lessons + booking.reload + booking.lesson_sessions.length.should eql 4 + + # check that all the times make sense + + lesson4 = booking.lesson_sessions[3] + lesson1.music_session.scheduled_start.to_i.should eql (lesson4.music_session.scheduled_start.to_i - (60 * 60 * 24 * 21)) + end + + it "ignores non-recurring" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + booking.lesson_sessions.length.should eql 1 + + booking.accept(booking.lesson_sessions[0], booking.lesson_booking_slots[0], user) + booking.errors.any?.should be false + booking.reload + booking.lesson_sessions.length.should eql 1 + booking.accepter.should eql user + end + end + + describe "canceling" do + + after do + Timecop.return + end + + it "single session gets canceled before accepted" do + + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + # avoid 24 hour problem + + UserMailer.deliveries.clear + Timecop.freeze(7.days.ago) + lesson_session.cancel({canceler: teacher_user, message: 'meh', slot: booking.default_slot.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.status.should eql LessonSession::STATUS_CANCELED + lesson_session.reload + booking.reload + booking.status.should eql LessonSession::STATUS_CANCELED + UserMailer.deliveries.length.should eql 1 + end + + it "single session gets canceled after accepted" do + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + lesson_session.accept({accepter: teacher_user, message: 'Yeah I got this', slot: booking.default_slot.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + lesson_session.reload + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + UserMailer.deliveries.clear + Timecop.freeze(7.days.ago) + lesson_session.cancel({canceler: user, message: 'meh', slot: booking.default_slot.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.status.should eql LessonSession::STATUS_CANCELED + booking.reload + booking.status.should eql LessonSession::STATUS_CANCELED + UserMailer.deliveries.length.should eql 2 + end + + it "recurring session gets canceled after accepted" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.active.should eql false + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + lesson_session.accept({accepter: teacher_user, message: 'Yeah I got this', slot: booking.default_slot.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + lesson_session.reload + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.reload + booking.active.should eql true + + UserMailer.deliveries.clear + Timecop.freeze(7.days.ago) + lesson_session.cancel({canceler: user, message: 'meh', slot: booking.default_slot.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_CANCELED + lesson_session.reload + lesson_session.canceler.should eql user + booking.reload + booking.active.should eql true + booking.status.should eql LessonSession::STATUS_APPROVED + booking.canceler.should be_nil + UserMailer.deliveries.length.should eql 2 + end + + it "recurring booking gets canceled after accepted" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + lesson_session.accept({accepter: teacher_user, message: 'Yeah I got this', slot: booking.default_slot.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + lesson_session.reload + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + UserMailer.deliveries.clear + Timecop.freeze(7.days.ago) + + mailer = mock + mailer.should_receive(:deliver_now).exactly(2).times + UserMailer.should_receive(:student_lesson_booking_canceled).and_return(mailer) + UserMailer.should_receive(:teacher_lesson_booking_canceled).and_return(mailer) + UserMailer.should_receive(:student_lesson_canceled).exactly(0).times + UserMailer.should_receive(:teacher_lesson_canceled).exactly(0).times + + lesson_session.cancel({canceler: user, message: 'meh', slot: booking.default_slot.id, update_all: true}) + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.status.should eql LessonSession::STATUS_CANCELED + booking.reload + booking.status.should eql LessonSession::STATUS_CANCELED + booking.canceler.should eql user + end + end + describe "rescheduling" do + + it "initial slot is in the past" do + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + + lesson_session = booking.lesson_sessions[0] + + initial_scheduled_time = lesson_session.scheduled_start + + counter = FactoryGirl.build(:lesson_booking_slot_single, preferred_day: Date.today + 20) + lesson_session.counter({proposer: user, slot: counter, message: 'ACtually, let\'s do this instead for just this one'}) + + Timecop.travel(initial_scheduled_time + 1) + + lesson_session.accept({accepter: teacher_user, message: 'Yeah I got this', slot: counter.id, update_all: false}) + booking.reload + booking.status.should eql LessonBooking::STATUS_APPROVED + booking.lesson_sessions.count.should eql 1 + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.status.should eql LessonSession::STATUS_APPROVED + lesson_session.scheduled_start.should eql counter.scheduled_time(0) + end + + it "non-recurring, accepted with new slot" do + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + counter = FactoryGirl.build(:lesson_booking_slot_single, hour: 16) + lesson_session.counter({proposer: user, slot: counter, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.status.should eql LessonSession::STATUS_COUNTERED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.reload + booking.counter_slot.should eql counter + + lesson_session.accept({accepter: teacher_user, message: 'Yeah I got this', slot: counter.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + lesson_session.reload + lesson_session.scheduled_start.should eql counter.scheduled_time(0) + booking.reload + booking.accepter.should eql teacher_user + booking.counter_slot.should be_nil + end + + it "recurring" do + Timecop.freeze(Time.new(2016, 03, 4, 5, 0, 0)) + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + counter = FactoryGirl.build(:lesson_booking_slot_recurring, day_of_week: 2, update_all: true) + lesson_session.counter({proposer: user, slot: counter, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.status.should eql LessonSession::STATUS_COUNTERED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + # to help scoot out the 'created_at' of the lessons + Timecop.freeze(Time.now + 10) + + lesson_session.accept({accepter: teacher_user, message: 'Yeah I got this', slot: counter.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.status.should eql LessonSession::STATUS_APPROVED + + lesson_session.reload + lesson_session.scheduled_start.should eql counter.scheduled_time(0) + + lesson_session2 = booking.lesson_sessions.order(:created_at)[1] + lesson_session2.scheduled_start.should eql counter.scheduled_time(1) + + # now it's approved, we have 2 sessions that are not yet completed with a time + + # we should be able to reschedule just one of the lessons + counter2 = FactoryGirl.build(:lesson_booking_slot_recurring, day_of_week: 4, update_all: false) + lesson_session.counter({proposer: user, slot: counter2, message: 'ACtually, let\'s do this instead for just this one'}) + lesson_session.errors.any?.should be false + lesson_session.status.should eql LessonSession::STATUS_COUNTERED + lesson_session.scheduled_start.should eql counter.scheduled_time(0) + + lesson_session.accept({accepter: teacher_user, message: 'OK, lets fix just this one', slot: counter2.id}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + lesson_session.reload + lesson_session2.reload + lesson_session.scheduled_start.should eql counter2.scheduled_time(0) + lesson_session2.scheduled_start.should eql counter.scheduled_time(1) # STILL ORIGINAL COUNTER! + + # we should be able to reschedule all of the lessons + + + counter3 = FactoryGirl.build(:lesson_booking_slot_recurring, day_of_week: 5, update_all: true) + lesson_session.counter({proposer: user, slot: counter3, message: 'ACtually, let\'s do this instead for just this one... again'}) + lesson_session.errors.any?.should be false + lesson_session.status.should eql LessonSession::STATUS_COUNTERED + lesson_session.reload + lesson_session2.reload + lesson_session.scheduled_start.should eql counter2.scheduled_time(0) + lesson_session2.scheduled_start.should eql counter.scheduled_time(1) + booking.reload + booking.counter_slot.should eql counter3 + + + lesson_session.accept({accepter: teacher_user, message: 'OK, lets fix all of them', slot: counter3.id}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.counter_slot.should be_nil + lesson_session.reload + lesson_session2.reload + lesson_session.created_at.should be < lesson_session2.created_at + + lesson_session.scheduled_start.should eql counter3.scheduled_time(0) + lesson_session2.scheduled_start.should eql counter3.scheduled_time(1 ) + + end + end +end diff --git a/ruby/spec/jam_ruby/models/lesson_package_purchase_spec.rb b/ruby/spec/jam_ruby/models/lesson_package_purchase_spec.rb new file mode 100644 index 000000000..5d542f0c4 --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_package_purchase_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' + +describe LessonPackagePurchase do + + let(:user) {FactoryGirl.create(:user)} + let(:lesson_booking) {FactoryGirl.create(:lesson_booking)} + +end diff --git a/ruby/spec/jam_ruby/models/lesson_session_analyser_spec.rb b/ruby/spec/jam_ruby/models/lesson_session_analyser_spec.rb new file mode 100644 index 000000000..29fe366a1 --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_session_analyser_spec.rb @@ -0,0 +1,309 @@ +require 'spec_helper' + +describe LessonSessionAnalyser do + + let(:user) { FactoryGirl.create(:user, remaining_test_drives: 1) } + let(:teacher) { FactoryGirl.create(:teacher_user) } + let(:start) { Time.now } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] } + let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } + + describe "analyse" do + + after{ + Timecop.return + } + it "neither show" do + lesson = testdrive_lesson(user, teacher) + music_session = lesson.music_session + + Timecop.freeze((lesson.music_session.scheduled_start + lesson.duration * 60) + 1) + + analysis = LessonSessionAnalyser.analyse(lesson) + analysis[:reason].should eql LessonSessionAnalyser::TEACHER_FAULT + analysis[:student].should eql nil + analysis[:bill].should be false + + student = analysis[:student_analysis] + student[:joined_on_time].should be false + student[:joined_late].should be false + student[:waited_correctly].should be false + student[:initial_waiting_pct].should eql nil + student[:potential_waiting_time].should eql nil + + together = analysis[:together_analysis] + together[:session_time].should eql 0 + end + + it "teacher joined on time, waited, student no show" do + lesson = testdrive_lesson(user, teacher) + music_session = lesson.music_session + + # create some bogus, super-perfect teacher/student times + start = lesson.scheduled_start + end_time = lesson.scheduled_start + (60 * lesson.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher, history: music_session, created_at: start, session_removed_at: end_time) + Timecop.travel(end_time + 1) + + + analysis = LessonSessionAnalyser.analyse(lesson) + analysis[:reason].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis[:student].should eql LessonSessionAnalyser::NO_SHOW + analysis[:bill].should be true + + student = analysis[:student_analysis] + student[:joined_on_time].should be false + student[:joined_late].should be false + student[:waited_correctly].should be false + student[:initial_waiting_pct].should eql nil + student[:potential_waiting_time].should eql nil + + together = analysis[:together_analysis] + together[:session_time].should eql 0 + end + + it "student joined 1 min before start time, is waiting for 12 minutes" do + lesson = testdrive_lesson(user, teacher) + music_session = lesson.music_session + + # create some bogus, super-perfect teacher/student times + start = lesson.scheduled_start + end_time = lesson.scheduled_start + (60 * lesson.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: user, history: music_session, created_at: start - 60, session_removed_at: nil) + + Timecop.freeze(start + 11 * 60) + + analysis = LessonSessionAnalyser.analyse(lesson, true) + analysis[:reason].should eql LessonSessionAnalyser::TEACHER_FAULT + analysis[:teacher].should eql LessonSessionAnalyser::NO_SHOW + analysis[:student].should be_nil + analysis[:bill].should be false + + student = analysis[:student_analysis] + student[:joined_on_time].should be true + student[:joined_late].should be false + student[:waited_correctly].should be true + student[:initial_waiting_pct].should eql 1.0 + student[:potential_waiting_time].should eql 600.0 + + student[:session_time].should eql (11 * 60).to_f + + together = analysis[:together_analysis] + together[:session_time].should eql 0 + end + + it "teacher joined on time, waited, student joined late" do + lesson = testdrive_lesson(user, teacher) + music_session = lesson.music_session + + # create some bogus, super-perfect teacher/student times + start = lesson.scheduled_start + end_time = lesson.scheduled_start + (60 * lesson.duration) + late_start = start + (7 * 60) + uh1 = FactoryGirl.create(:music_session_user_history, user: user, history: music_session, created_at: late_start, session_removed_at: late_start + 4 * 60) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher, history: music_session, created_at: start, session_removed_at: end_time) + + Timecop.travel(end_time + 1) + + + analysis = LessonSessionAnalyser.analyse(lesson) + analysis[:reason].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis[:student].should eql LessonSessionAnalyser::JOINED_LATE + analysis[:bill].should be true + + student = analysis[:student_analysis] + student[:joined_on_time].should be false + student[:joined_late].should be true + student[:waited_correctly].should be false + student[:initial_waiting_pct].should eql nil + student[:potential_waiting_time].should eql nil + + together = analysis[:together_analysis] + together[:session_time].should eql (4 * 60.to_f) + end + + it "together 5 minutes" do + + lesson = testdrive_lesson(user, teacher) + music_session = lesson.music_session + + analysis = LessonSessionAnalyser.analyse(lesson) + analysis[:reason].should eql LessonSessionAnalyser::SESSION_ONGOING + + + # create some bogus, super-perfect teacher/student times + start = lesson.scheduled_start + end_time = lesson.scheduled_start + (60 * lesson.duration) + uh1 = FactoryGirl.create(:music_session_user_history, user: user, history: music_session, created_at: start, session_removed_at: end_time) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher, history: music_session, created_at: start, session_removed_at: end_time) + + + analysis = LessonSessionAnalyser.analyse(lesson) + analysis[:reason].should eql LessonSessionAnalyser::SESSION_ONGOING + + Timecop.travel(end_time + 1) + + analysis = LessonSessionAnalyser.analyse(lesson) + analysis[:reason].should eql LessonSessionAnalyser::SUCCESS + analysis[:bill].should be true + + teacher = analysis[:teacher_analysis] + teacher[:joined_on_time].should be true + teacher[:waited_correctly].should be true + teacher[:initial_waiting_pct].should eql 1.0 + teacher[:potential_waiting_time].should eql 600.0 + + together = analysis[:together_analysis] + together[:session_time].should eql 1800.0 + end + end + + describe "intersecting_ranges" do + + let(:lesson) { testdrive_lesson(user, teacher) } + let(:music_session) { lesson.music_session } + + it "empty" do + LessonSessionAnalyser.intersecting_ranges([], []).should eql [] + end + + it "one specified, other empty" do + + uh1Begin = start + uh1End = start + 1 + + LessonSessionAnalyser.intersecting_ranges([], [Range.new(uh1Begin, uh1End)]).should eql [] + LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], []).should eql [] + end + + it "both identical" do + uh1Begin = start + uh1End = start + 1 + + LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], [Range.new(uh1Begin, uh1End)]).should eql [Range.new(uh1Begin, uh1End)] + end + + it "one intersect" do + uh1Begin = start + uh1End = start + 4 + + uh2Begin = start + 1 + uh2End = start + 3 + + LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], [Range.new(uh2Begin, uh2End)]).should eql [Range.new(uh2Begin, uh2End)] + end + + it "overlapping intersect" do + uh1Begin = start + uh1End = start + 4 + + uh2Begin = start + -1 + uh2End = start + 2 + + uh3Begin = start + 3 + uh3End = start + 5 + + LessonSessionAnalyser.intersecting_ranges([Range.new(uh2Begin, uh2End), Range.new(uh3Begin, uh3End)], [Range.new(uh1Begin, uh1End)]).should eql [Range.new(uh1Begin, uh2End), Range.new(uh3Begin, uh1End)] + end + + it "no overlap" do + uh1Begin = start + uh1End = start + 4 + + uh2Begin = start + 5 + uh2End = start + 6 + + LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], [Range.new(uh2Begin, uh2End)]).should eql [] + end + end + + describe "merge_overlapping_ranges" do + + let(:lesson) { testdrive_lesson(user, teacher) } + let(:music_session) { lesson.music_session } + + + it "empty" do + LessonSessionAnalyser.merge_overlapping_ranges([]).should eql [] + end + + it "one item" do + uh1Begin = start + uh1End = start + 1 + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + + ranges = LessonSessionAnalyser.time_ranges [uh1] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh1End)] + end + + it "two identical items" do + uh1Begin = start + uh1End = start + 1 + + uh2Begin = uh1Begin + uh2End = uh1End + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End) + + ranges = LessonSessionAnalyser.time_ranges [uh1, uh2] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh1End)] + end + + it "two separate times" do + uh1Begin = start + uh1End = start + 1 + + uh2Begin = start + 3 + uh2End = start + 5 + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End) + + ranges = LessonSessionAnalyser.time_ranges [uh1, uh2] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh1End), Range.new(uh2Begin, uh2End)] + end + + it "two overlapping times" do + uh1Begin = start + uh1End = start + 1 + + uh2Begin = start + 0.5 + uh2End = start + 5 + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End) + + ranges = LessonSessionAnalyser.time_ranges [uh1, uh2] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh2End)] + end + + it "three overlapping times" do + uh1Begin = start + uh1End = start + 1 + + uh2Begin = start + 0.5 + uh2End = start + 5 + + uh3Begin = start + 0.1 + uh3End = start + 6 + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End) + uh3 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh3Begin, session_removed_at: uh3End) + + ranges = LessonSessionAnalyser.time_ranges [uh1, uh2, uh3] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh3End)] + end + end +end diff --git a/ruby/spec/jam_ruby/models/lesson_session_monthly_price_spec.rb b/ruby/spec/jam_ruby/models/lesson_session_monthly_price_spec.rb new file mode 100644 index 000000000..0f254d5e7 --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_session_monthly_price_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe LessonSessionMonthlyPrice do + + let(:user) { FactoryGirl.create(:user) } + let(:teacher) { FactoryGirl.create(:teacher_user) } + let(:start) { Time.now } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] } + let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } + + describe "price" do + after do + Timecop.return + end + + it "start of the month" do + + jan1 = Date.new(2016, 1, 1) + Timecop.freeze(jan1) + + booking = LessonBooking.book_normal(user, teacher, valid_recurring_slots, "Monthly time!", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + + booking.booked_price.should eql 30.00 + + booking.booked_price.should eql(LessonSessionMonthlyPrice.price(booking, jan1)) + end + + it "middle of the month" do + + jan17 = Date.new(2016, 1, 17) + Timecop.freeze(jan17) + + booking = LessonBooking.book_normal(user, teacher, valid_recurring_slots, "Monthly time!", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + + booking.booked_price.should eql 30.00 + + ((booking.booked_price * 0.75).round(2)).should eql(LessonSessionMonthlyPrice.price(booking, jan17)) + end + + it "end of the month which has a last billable day based on slot" do + + jan17 = Date.new(2016, 1, 31) + Timecop.freeze(jan17) + + booking = LessonBooking.book_normal(user, teacher, valid_recurring_slots, "Monthly time!", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + + booking.booked_price.should eql 30.00 + + ((booking.booked_price * 0.25).round(2)).should eql(LessonSessionMonthlyPrice.price(booking, jan17)) + end + + it "end of the month which is not a last billable days" do + + feb29 = Date.new(2016, 2, -1) + Timecop.freeze(feb29) + + booking = LessonBooking.book_normal(user, teacher, valid_recurring_slots, "Monthly time!", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + + booking.booked_price.should eql 30.00 + + ((booking.booked_price * 0.0).round(2)).should eql(LessonSessionMonthlyPrice.price(booking, feb29)) + end + end +end diff --git a/ruby/spec/jam_ruby/models/lesson_session_spec.rb b/ruby/spec/jam_ruby/models/lesson_session_spec.rb new file mode 100644 index 000000000..7ad298f69 --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_session_spec.rb @@ -0,0 +1,345 @@ +require 'spec_helper' + +describe LessonSession do + + let(:user) {FactoryGirl.create(:user, stored_credit_card: true, remaining_free_lessons: 1, remaining_test_drives: 1)} + let(:teacher) {FactoryGirl.create(:teacher_user)} + let(:slot1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:slot2) { FactoryGirl.build(:lesson_booking_slot_single) } + + let(:lesson_booking) {b = LessonBooking.book_normal(user, teacher, [slot1, slot2], "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60); b.card_presumed_ok = true; b.save!; b} + let(:lesson_session) {lesson_booking.lesson_sessions[0]} + + describe "counter" do + describe "recurring" do + it "counter madness" do + lesson = monthly_lesson(user, teacher, {accept:false}) + # start with the student + invalid = FactoryGirl.build(:lesson_booking_slot_single, update_all: false) + + lesson.counter({proposer: user, message: "crumble and bumble", slot: invalid}) + lesson.errors.any?.should be_true + lesson.errors[:counter_slot].should eql ["Only 'update all' counter-proposals are allowed for un-approved, recurring lessons", "Only 'recurring' counter-proposals are allowed for un-approved, recurring lessons"] + lesson.reload + lesson.counter_slot.should be_nil + + counter1 = FactoryGirl.build(:lesson_booking_slot_recurring, update_all: true) + + lesson.counter({proposer: user, message: "crumble and bumble take 2", slot: counter1}) + lesson.errors.any?.should be_false + lesson.reload + lesson.status.should eql LessonSession::STATUS_COUNTERED + lesson.counter_slot.id.should eql counter1.id + lesson.lesson_booking.counter_slot.id.should eql counter1.id + + counter2 = FactoryGirl.build(:lesson_booking_slot_recurring, update_all: true) + + lesson.counter({proposer: teacher, message: "crumble and bumble take 3", slot: counter2}) + lesson.errors.any?.should be_false + lesson.reload + lesson.status.should eql LessonSession::STATUS_COUNTERED + lesson.counter_slot.id.should eql counter2.id + lesson.lesson_booking.counter_slot.id.should eql counter2.id + + lesson.accept({accepter: user, message: "burp", slot: counter2.id}) + lesson.errors.any?.should be_false + lesson.reload + lesson.status.should eql LessonSession::STATUS_APPROVED + + counter3 = FactoryGirl.build(:lesson_booking_slot_recurring, update_all: false) + + lesson.counter({proposer: user, message: "crumble and bumble take 4", slot: counter3}) + lesson.errors.any?.should be_false + lesson.reload + lesson.status.should eql LessonSession::STATUS_COUNTERED + lesson.counter_slot.id.should eql counter3.id + lesson.lesson_booking.counter_slot.id.should eql counter3.id + + counter4 = FactoryGirl.build(:lesson_booking_slot_recurring, update_all: true) + + lesson.counter({proposer: teacher, message: "crumble and bumble take 5", slot: counter4}) + lesson.errors.any?.should be_false + lesson.reload + lesson.status.should eql LessonSession::STATUS_COUNTERED + lesson.counter_slot.id.should eql counter4.id + lesson.lesson_booking.counter_slot.id.should eql counter4.id + end + end + end + + describe "autocancel" do + it "can't autocancel in the past" do + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + + lesson_session.autocancel + lesson_session.reload + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + lesson_session.lesson_booking.status.should eql LessonSession::STATUS_REQUESTED + end + + it "can't autocancel approved" do + lesson_session = normal_lesson(user, teacher, {accept: true}) + lesson_session.status.should eql LessonSession::STATUS_APPROVED + + lesson_session.autocancel + lesson_session.reload + lesson_session.status.should eql LessonSession::STATUS_APPROVED + lesson_session.lesson_booking.status.should eql LessonSession::STATUS_APPROVED + end + + it "autocancel works" do + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + + Timecop.travel(Date.today + 10) + + lesson_session.autocancel + lesson_session.reload + lesson_session.status.should eql LessonSession::STATUS_UNCONFIRMED + lesson_session.lesson_booking.status.should eql LessonSession::STATUS_UNCONFIRMED + end + + it "autocancel sweeper works" do + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + + Timecop.travel(Date.today + 10) + + LessonSession.auto_cancel + lesson_session.reload + lesson_session.lesson_booking.active.should be_false + lesson_session.status.should eql LessonSession::STATUS_UNCONFIRMED + lesson_session.lesson_booking.status.should eql LessonSession::STATUS_UNCONFIRMED + end + end + + describe "slow_responses" do + it "sorts correctly" do + lesson_session1 = normal_lesson(user, teacher, {counter: true, counterer: user}) + lesson_session2 = normal_lesson(user, teacher, {counter: true, counterer: teacher}) # this shouldn't show up + Timecop.travel(Date.today - 5) + lesson_session3 = normal_lesson(user, teacher, {}) + + slow = LessonSession.unscoped.slow_responses + slow.count.should eql 2 + slow[0].should eql lesson_session3 + slow[1].should eql lesson_session1 + end + end + describe "least_time_left" do + it "sorts correctly" do + lesson_session1 = normal_lesson(user, teacher, {counter: true, counterer: user}) + lesson_session2 = normal_lesson(user, teacher, {counter: true, counterer: teacher}) # this shouldn't show up + Timecop.travel(Date.today - 5) + lesson_session3 = normal_lesson(user, teacher, {}) + + slow = LessonSession.unscoped.least_time_left + slow.count.should eql 2 + slow[0].should eql lesson_session1 + slow[1].should eql lesson_session3 + end + end + + describe "remind_counters" do + it "finds old requested and pokes teacher" do + lesson_session1 = normal_lesson(user, teacher, {}) + + mailer = mock + mailer.should_receive(:deliver!) + UserMailer.should_receive(:teacher_counter_reminder).and_return(mailer) + + LessonSession.remind_counters + lesson_session1.reload + lesson_session1.lesson_booking.sent_counter_reminder.should be_false + + Timecop.travel(Date.today + 10) + + LessonSession.remind_counters + lesson_session1.reload + lesson_session1.lesson_booking.sent_counter_reminder.should be_true + end + + it "finds old counter and pokes teacher" do + lesson_session1 = normal_lesson(user, teacher, {counter: true, counterer: user}) + + mailer = mock + mailer.should_receive(:deliver!) + UserMailer.should_receive(:teacher_counter_reminder).and_return(mailer) + + LessonSession.remind_counters + lesson_session1.reload + lesson_session1.lesson_booking.sent_counter_reminder.should be_false + + Timecop.travel(Date.today + 10) + + LessonSession.remind_counters + lesson_session1.reload + lesson_session1.lesson_booking.sent_counter_reminder.should be_true + end + + it "finds old counter and pokes teacher" do + lesson_session1 = normal_lesson(user, teacher, {counter: true, counterer: teacher}) + + mailer = mock + mailer.should_receive(:deliver!) + UserMailer.should_receive(:student_counter_reminder).and_return(mailer) + + LessonSession.remind_counters + lesson_session1.reload + lesson_session1.lesson_booking.sent_counter_reminder.should be_false + + Timecop.travel(Date.today + 10) + + LessonSession.remind_counters + lesson_session1.reload + lesson_session1.lesson_booking.sent_counter_reminder.should be_true + end + end + + describe "accept" do + it "accepts in the past is errored" do + slot = FactoryGirl.build(:lesson_booking_slot_single, preferred_day: Date.today + 1) + lesson = normal_lesson(user, teacher, {slots: [slot, slot2]}) + Timecop.travel(Date.today + 2) + lesson.accept({ + message: "Teacher time!", + acceptor: teacher, + slot: slot.id + }) + lesson.errors.any?.should be_true + lesson.errors[:slot].should eql ["is in the past"] + end + + it "accepts in the past for a recurring is OK" do + slotRecurring1 = FactoryGirl.build(:lesson_booking_slot_recurring) + slotRecurring2 = FactoryGirl.build(:lesson_booking_slot_recurring) + lesson = monthly_lesson(user, teacher, {slots: [slotRecurring1, slotRecurring2]}) + Timecop.travel(Date.today + 100) + lesson.accept({ + message: "Teacher time!", + acceptor: teacher, + slot: slotRecurring1.id + }) + lesson.errors.any?.should be_true + lesson.errors[:slot].should eql ["is in the past"] + end + + it "cancel in the past is OK" do + slot = FactoryGirl.build(:lesson_booking_slot_single, preferred_day: Date.today + 1) + lesson = normal_lesson(user, teacher, {slots: [slot, slot2]}) + + Timecop.travel(Date.today + 2) + lesson.cancel({ + message: "Cancel time!", + canceler: teacher, + slot: slot + }) + lesson.errors.any?.should be_false + lesson.reload + lesson.status.should eql LessonSession::STATUS_CANCELED + lesson.lesson_booking.status.should eql LessonSession::STATUS_CANCELED + end + end + + describe "permissions" do + it "student can join session" do + lesson = normal_lesson(user, teacher) + + FactoryGirl.create(:active_music_session, music_session: lesson.music_session, creator: teacher) + + connection1 = FactoryGirl.create(:connection, user: user, music_session_id: lesson.music_session.active_music_session.id, as_musician: true) + connection2 = FactoryGirl.create(:connection, user: teacher, music_session_id: lesson.music_session.active_music_session.id, as_musician: true) + + connection1.can_join_music_session + connection1.errors.any?.should be_false + + connection2.can_join_music_session + connection2.errors.any?.should be_false + end + end + describe "upcoming_sessions_reminder" do + it "succeeds" do + lesson = normal_lesson(user, teacher) + UserMailer.deliveries.clear + LessonSession.upcoming_sessions_reminder + #UserMailer.deliveries.count.should eql 2 + lesson.touch + lesson.sent_starting_notice.should be_false + lesson.is_approved?.should be_true + lesson.music_session.scheduled_start = 15.minutes.from_now + lesson.music_session.save! + LessonSession.upcoming_sessions_reminder + UserMailer.deliveries.count.should eql 2 + UserMailer.deliveries.clear + lesson.reload + lesson.sent_starting_notice.should be_true + LessonSession.upcoming_sessions_reminder + UserMailer.deliveries.count.should eql 0 + end + end + + describe "index" do + it "finds single lesson as student" do + + lesson_session.touch + lesson_session.music_session.creator.should eql lesson_session.lesson_booking.user + lesson_session.lesson_booking.teacher.should eql teacher + + query = LessonSession.index(user)[:query] + query.length.should eq 1 + + # make sure some random nobody can see this lesson session + query = LessonSession.index(FactoryGirl.create(:user))[:query] + query.length.should eq 0 + end + + it "finds single lesson as teacher" do + + # just sanity check that the lesson_session Factory is doing what it should + lesson_session.music_session.creator.should eql lesson_session.lesson_booking.user + lesson_session.lesson_booking.teacher.should eql teacher + + query = LessonSession.index(teacher, {as_teacher: true})[:query] + query.length.should eq 1 + + # make sure some random nobody can see this lesson session + query = LessonSession.index(FactoryGirl.create(:user), {as_teacher: true})[:query] + query.length.should eq 0 + end + + describe "schools" do + let (:school) {FactoryGirl.create(:school, scheduling_communication: School::SCHEDULING_COMM_SCHOOL)} + describe "owner" do + + it "works when not a teacher" do + query = LessonSession.index(school.owner, {as_teacher: true})[:query] + query.length.should eql 0 + + teacher.teacher.school = school + teacher.teacher.save! + teacher.reload + school.reload + + school.teachers.to_a.should eql [teacher.teacher] + lesson = normal_lesson(user, teacher, {accept: false}) + lesson.status.should eql LessonSession::STATUS_REQUESTED + lesson.lesson_booking.school.should eql school + school.owner.reload + query = LessonSession.index(school.owner, {as_teacher: true})[:query] + query.length.should eql 1 + + query = LessonSession.index(teacher, {as_teacher: true})[:query] + query.length.should eql 0 + + + lesson = normal_lesson(user, teacher, {accept: true}) + lesson.status.should eql LessonSession::STATUS_APPROVED + lesson.lesson_booking.school.should eql school + query = LessonSession.index(school.owner, {as_teacher: true})[:query] + query.length.should eql 1 + + query = LessonSession.index(teacher, {as_teacher: true})[:query] + query.length.should eql 1 + end + end + end + end +end diff --git a/ruby/spec/jam_ruby/models/music_session_spec.rb b/ruby/spec/jam_ruby/models/music_session_spec.rb index eb68b96db..154aadb67 100644 --- a/ruby/spec/jam_ruby/models/music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/music_session_spec.rb @@ -104,7 +104,7 @@ describe MusicSession do it "displays central time correctly" do time = MusicSession.parse_scheduled_start("Thu Jul 10 2014 10:00 PM", "Central Time (US & Canada),America/Chicago") music_session = FactoryGirl.create(:music_session, scheduled_start: time, timezone: "Central Time (US & Canada),America/Chicago") - music_session.pretty_scheduled_start(true).should == 'Thursday, July 10, 10:00-11:00 PM Central Time (US & Canada)' + music_session.pretty_scheduled_start(true).should == 'Thursday, July 10, 10:00-11:00 PM US Central Time' music_session.pretty_scheduled_start(false).should == 'Thursday, July 10 - 10:00pm' end @@ -113,6 +113,13 @@ describe MusicSession do music_session.pretty_scheduled_start(true).should == 'Date and time TBD' music_session.pretty_scheduled_start(false).should == 'Date and time TBD' end + + it "displays default correctly (shorter)" do + time = MusicSession.parse_scheduled_start("Thu Jul 10 2014 10:00 PM", "Central Time (US & Canada),America/Chicago") + music_session = FactoryGirl.create(:music_session, scheduled_start: time, timezone: "Central Time (US & Canada),America/Chicago") + puts music_session.pretty_scheduled_start(true, true) + puts music_session.pretty_scheduled_start(true, false) + end end describe "nindex" do @@ -362,6 +369,31 @@ describe MusicSession do end describe "scheduled" do + + it "includes any RSVP'ed" do + rsvp_request = FactoryGirl.create(:rsvp_request_for_multiple_slots, user: some_user, music_session: music_session1, number: 2, chosen:true) + + approved_rsvps = music_session1.approved_rsvps + approved_rsvps.length.should == 2 + + + sessions = MusicSession.scheduled(approved_rsvps[0]) + sessions.length.should == 1 + + sessions = MusicSession.scheduled(approved_rsvps[1]) + sessions.length.should == 1 + end + + it "includes invited" do + invitee = FactoryGirl.create(:user, last_jam_audio_latency: 30, last_jam_locidispid: 3) + FactoryGirl.create(:friendship, user: creator, friend: invitee) + FactoryGirl.create(:friendship, user: invitee, friend: creator) + music_session = FactoryGirl.create(:music_session, creator: creator) + FactoryGirl.create(:invitation, receiver:invitee, sender:creator, music_session: music_session) + + sessions = MusicSession.scheduled(invitee) + sessions.length.should == 1 + end it "excludes based on time-range" do session = FactoryGirl.create(:music_session, scheduled_start: Time.now) @@ -512,7 +544,6 @@ describe MusicSession do dd = Time.now - (interval.to_i + 1).days Timecop.travel(dd) msess1 = FactoryGirl.create(:music_session, creator: creator, scheduled_start: dd) - Timecop.return msess2 = FactoryGirl.create(:music_session, creator: creator) music_sessions, user_scores = sms(searcher, default_opts, 1) expect(music_sessions[0].id).to eq(msess2.id) @@ -866,7 +897,6 @@ describe MusicSession do dd = Time.now - (interval.to_i + 1).days Timecop.travel(dd) msess1 = FactoryGirl.create(:music_session) - Timecop.return msess2 = FactoryGirl.create(:music_session) purging = MusicSession.purgeable_sessions expect(purging.size).to be(1) @@ -879,7 +909,6 @@ describe MusicSession do dd = Time.now - (interval.to_i + 1).days Timecop.travel(dd) msess1 = FactoryGirl.create(:music_session, scheduled_start: Time.now) - Timecop.return msess2 = FactoryGirl.create(:music_session, scheduled_start: Time.now) purging = MusicSession.purgeable_sessions expect(purging.size).to be(1) diff --git a/ruby/spec/jam_ruby/models/music_sessions_user_history_spec.rb b/ruby/spec/jam_ruby/models/music_sessions_user_history_spec.rb index 42415be4d..edd19bbc1 100644 --- a/ruby/spec/jam_ruby/models/music_sessions_user_history_spec.rb +++ b/ruby/spec/jam_ruby/models/music_sessions_user_history_spec.rb @@ -7,6 +7,9 @@ describe MusicSessionUserHistory do let(:user_history1) { FactoryGirl.create(:music_session_user_history, :history => music_session.music_session, :user => music_session.creator) } let(:user_history2) { FactoryGirl.create(:music_session_user_history, :history => music_session.music_session, :user => some_user) } + after { + Timecop.return + } describe "create" do pending it {user_history1.music_session_id.should == music_session.id } @@ -31,7 +34,6 @@ describe MusicSessionUserHistory do users = [user_history1, user_history2] Timecop.travel(Time.now + (MusicSessionUserHistory::MIN_SESSION_DURATION_RATING * 1.5).seconds) expect( user_history1.should_rate_session? ).to eq(true) - Timecop.return end it 'should rate fails' do diff --git a/ruby/spec/jam_ruby/models/musician_search_spec.rb b/ruby/spec/jam_ruby/models/musician_search_spec.rb index bfd46c1c2..af9715b38 100644 --- a/ruby/spec/jam_ruby/models/musician_search_spec.rb +++ b/ruby/spec/jam_ruby/models/musician_search_spec.rb @@ -238,6 +238,7 @@ describe 'Musician Search Model' do end it "sorts by latency", intermittent: true do + pending search.update_json_value(MusicianSearch::KEY_SORT_ORDER, MusicianSearch::SORT_VALS[0]) results = search.do_search expect(results[0].id).to eq(@user1.id) # HAS FAILED HERE TOO 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 ba89aee20..bce945d20 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 @@ -135,7 +135,7 @@ describe RecurlyTransactionWebHook do RecurlyTransactionWebHook.create_from_xml(document) - JamTrackRight.find_by_id(jam_track_right.id).should be_nil + JamTrackRight.find_by_id(jam_track_right.id).should_not be_nil end it "deletes jam_track_right when voided" do @@ -154,7 +154,7 @@ describe RecurlyTransactionWebHook do RecurlyTransactionWebHook.create_from_xml(document) - JamTrackRight.find_by_id(jam_track_right.id).should be_nil + JamTrackRight.find_by_id(jam_track_right.id).should_not be_nil end end diff --git a/ruby/spec/jam_ruby/models/review_spec.rb b/ruby/spec/jam_ruby/models/review_spec.rb new file mode 100644 index 000000000..3deed6f3b --- /dev/null +++ b/ruby/spec/jam_ruby/models/review_spec.rb @@ -0,0 +1,169 @@ +require 'spec_helper' + +describe Review do + + shared_examples_for :review do |target, target_type| + before(:each) do + Review.delete_all + User.delete_all + @user = FactoryGirl.create(:user) + end + + after(:all) do + Review.delete_all + User.delete_all + end + + context "validates review" do + it "blank target" do + review = Review.create({}) + review.valid?.should be_false + review.errors[:target].should == ["can't be blank"] + end + + it "no rating" do + review = Review.create(target:target) + review.valid?.should be_false + review.errors[:rating].should include("can't be blank") + review.errors[:rating].should include("is not a number") + end + + it "no user" do + review = Review.create(target:target, rating:3) + review.valid?.should be_false + review.errors[:user_id].should include("can't be blank") + end + + it "complete" do + review = Review.create(target:target, rating:3, user:@user) + review.valid?.should be_true + end + + it "unique" do + review = Review.create(target:target, rating:3, user:@user) + review.valid?.should be_true + + review2 = Review.create(target:target, rating:3, user:@user) + review2.valid?.should be_false + end + + it "reduces" do + review = Review.create(target:target, rating:3, user:@user) + review.valid?.should be_true + + review2 = Review.create(target:target, rating:5, user:FactoryGirl.create(:user)) + review2.valid?.should be_true + Review.should have(2).items + Review.index.should have(2).items + + # Reduce and check: + ReviewSummary.should have(1).items + ReviewSummary.first.avg_rating.should eq(4.0) + + ws_orig = ReviewSummary.first.wilson_score + avg_orig = ReviewSummary.first.avg_rating + + # Create some more and verify: + 5.times {Review.create(target:target, rating:5, user:FactoryGirl.create(:user))} + Review.index.should have(7).items + ReviewSummary.should have(1).items + + ReviewSummary.first.wilson_score.should > ws_orig + ReviewSummary.first.avg_rating.should > avg_orig + + end + end # context + + context "validates review summary" do + it "blank target" do + review_summary = ReviewSummary.create() + review_summary.valid?.should be_false + review_summary.errors[:target_id].should == ["can't be blank"] + end + + it "no rating" do + review_summary = ReviewSummary.create(target:target) + review_summary.valid?.should be_false + review_summary.errors[:target].should be_empty + review_summary.errors[:avg_rating].should include("can't be blank") + review_summary.errors[:avg_rating].should include("is not a number") + end + + it "no score" do + review_summary = ReviewSummary.create(target:target, avg_rating:3.2) + review_summary.valid?.should be_false + review_summary.errors[:target].should be_empty + review_summary.errors[:avg_rating].should be_empty + review_summary.errors[:wilson_score].should include("can't be blank") + review_summary.errors[:wilson_score].should include("is not a number") + end + + it "no count" do + review_summary = ReviewSummary.create(target:target, avg_rating:3.2, wilson_score:0.95) + review_summary.valid?.should be_false + review_summary.errors[:review_count].should include("can't be blank") + end + + it "complete" do + review_summary = ReviewSummary.create(target:target, avg_rating:3.2, wilson_score:0.95, review_count: 15) + review_summary.valid?.should be_true + end + + it "unique" do + review = ReviewSummary.create(target:target, avg_rating:3, wilson_score:0.82, review_count:14) + review.valid?.should be_true + + review2 = ReviewSummary.create(target:target, avg_rating:3.22, wilson_score:0.91, review_count:12) + review2.valid?.should be_false + end + + it "reduces and queries" do + review = Review.create(target:target, rating:3, user:@user) + review.valid?.should be_true + review2 = Review.create(target:target, rating:5, user:FactoryGirl.create(:user)) + review2.valid?.should be_true + Review.should have(2).items + + ReviewSummary.should have(1).items + ReviewSummary.first.avg_rating.should eq(4.0) + + ws_orig = ReviewSummary.first.wilson_score + avg_orig = ReviewSummary.first.avg_rating + + + # Create some more and verify: + 5.times {Review.create(target:target, rating:5, user:FactoryGirl.create(:user))} + ReviewSummary.should have(1).items + ReviewSummary.first.wilson_score.should > ws_orig + ReviewSummary.first.avg_rating.should > avg_orig + + + # Create some more with a different target and verify: + target2=FactoryGirl.create(:jam_track) + 5.times {Review.create(target:target2, rating:5, user:FactoryGirl.create(:user))} + Review.index.should have(12).items + Review.index(target_id: target2).should have(5).items + summaries = ReviewSummary.index() + summaries.should have(2).items + summaries[0].wilson_score.should > summaries[1].wilson_score + + summaries = ReviewSummary.index(target_id: target2) + summaries.should have(1).items + summaries[0].target_id.should eq(target2.id) + + summaries = ReviewSummary.index(target_type: "JamRuby::JamTrack") + summaries.should have(2).items + + summaries = ReviewSummary.index(minimum_reviews: 6) + summaries.should have(1).items + end + end + end + + describe "with a jamtrack" do + @jam_track = FactoryGirl.create(:jam_track) + it_behaves_like :review, @jam_track, "jam_track" + end + + +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/sale_line_item_spec.rb b/ruby/spec/jam_ruby/models/sale_line_item_spec.rb index 334166734..4d0340259 100644 --- a/ruby/spec/jam_ruby/models/sale_line_item_spec.rb +++ b/ruby/spec/jam_ruby/models/sale_line_item_spec.rb @@ -6,6 +6,7 @@ describe SaleLineItem do let(:user) {FactoryGirl.create(:user)} let(:user2) {FactoryGirl.create(:user)} let(:jam_track) {FactoryGirl.create(:jam_track)} + let(:gift_card) {FactoryGirl.create(:gift_card_type, card_type: GiftCardType::JAM_TRACKS_10)} describe "associations" do @@ -23,7 +24,7 @@ describe SaleLineItem do describe "state" do - it "success" do + it "jam track 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) @@ -37,5 +38,20 @@ describe SaleLineItem do success: true }) end + + it "gift card success" do + sale = Sale.create_jam_track_sale(user) + shopping_cart = ShoppingCart.create(user, gift_card) + 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 685d8dfbd..7378cbb2a 100644 --- a/ruby/spec/jam_ruby/models/sale_spec.rb +++ b/ruby/spec/jam_ruby/models/sale_spec.rb @@ -2,9 +2,30 @@ require 'spec_helper' describe Sale do - let(:user) {FactoryGirl.create(:user)} - let(:user2) {FactoryGirl.create(:user)} - let(:jam_track) {FactoryGirl.create(:jam_track)} + let(:user) { FactoryGirl.create(:user) } + let(:user2) { FactoryGirl.create(:user) } + let(:jam_track) { FactoryGirl.create(:jam_track) } + let(:jam_track2) { FactoryGirl.create(:jam_track) } + let(:jam_track3) { FactoryGirl.create(:jam_track) } + let(:gift_card) { GiftCardType.jam_track_5 } + + after(:each) { + Timecop.return + } + def assert_free_line_item(sale_line_item, jamtrack) + sale_line_item.recurly_tax_in_cents.should be_nil + sale_line_item.recurly_total_in_cents.should be_nil + sale_line_item.recurly_currency.should be_nil + sale_line_item.recurly_discount_in_cents.should be_nil + 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) + end describe "index" do it "empty" do @@ -45,9 +66,13 @@ describe Sale do describe "place_order" do - let(:user) {FactoryGirl.create(:user)} + let(:user) { FactoryGirl.create(:user) } let(:jamtrack) { FactoryGirl.create(:jam_track) } + let(:jamtrack2) { FactoryGirl.create(:jam_track) } + let(:jamtrack3) { FactoryGirl.create(:jam_track) } + let(:jamtrack4) { FactoryGirl.create(:jam_track) } let(:jam_track_price_in_cents) { (jamtrack.price * 100).to_i } + let(:gift_card_price_in_cents) { (gift_card.price * 100).to_i } let(:client) { RecurlyClient.new } let(:billing_info) { info = {} @@ -75,6 +100,77 @@ describe Sale do end end + it "for a gift card" do + shopping_cart = ShoppingCart.create user, gift_card, 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(gift_card_price_in_cents) + sale.recurly_tax_in_cents.should eq(0) + sale.recurly_total_in_cents.should eq(gift_card_price_in_cents) + sale.recurly_currency.should eq('USD') + + sale.order_total.should eq(gift_card.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(gift_card_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(GiftCardType::PRODUCT_TYPE) + sale_line_item.unit_price.should eq(gift_card.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(gift_card.plan_code) + sale_line_item.product_id.should eq(gift_card.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_not be_nil + + # 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((gift_card.price * 100).to_i) + purchase.accounting_code.should eq(ShoppingCart::PURCHASE_NORMAL) + purchase.description.should eq("JamTracks Gift Card (5)") + 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((gift_card.price * 100).to_i) + invoice.total_in_cents.should eq((gift_card.price * 100).to_i) + invoice.state.should eq('collected') + + # verify jam_track_rights data + user.gift_card_purchases.should_not be_nil + user.gift_card_purchases.should have(1).items + user.gift_card_purchases.last.gift_card_type.should eq(GiftCardType.jam_track_5) + user.has_redeemable_jamtrack.should be_true + + sale_line_item.affiliate_referral.should be_nil + sale_line_item.affiliate_referral_fee_in_cents.should be_nil + end + it "for a free jam track" do shopping_cart = ShoppingCart.create user, jamtrack, 1, true @@ -87,6 +183,7 @@ describe Sale do sales.should eq(user.sales) sale = sales[0] + sale.recurly_invoice_id.should be_nil sale.recurly_subtotal_in_cents.should eq(0) @@ -132,6 +229,68 @@ describe Sale do user.has_redeemable_jamtrack.should be_false end + it "for two jam tracks (1 freebie, 1 gifted), then 1 gifted/1 pay" do + user.gifted_jamtracks = 2 + user.save! + + shopping_cart1 = ShoppingCart.create user, jamtrack, 1, true + shopping_cart2 = ShoppingCart.create user, jamtrack2, 1, true + + client.find_or_create_account(user, billing_info) + + sales = Sale.place_order(user, [shopping_cart1, shopping_cart2]) + + user.reload + user.sales.length.should eq(1) + sale = sales[0] + sale.reload + + 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 == 2 + + assert_free_line_item(sale.sale_line_items[0], jamtrack) + assert_free_line_item(sale.sale_line_items[1], jamtrack2) + + # verify jam_track_rights data + right1 = JamTrackRight.where(user_id: user.id).where(jam_track_id: jamtrack.id).first + right2 = JamTrackRight.where(user_id: user.id).where(jam_track_id: jamtrack2.id).first + user.jam_track_rights.should have(2).items + + right1.redeemed.should be_true + right2.redeemed.should be_true + user.has_redeemable_jamtrack.should be_false + user.gifted_jamtracks.should eq(1) + + + # OK! Now make a second purchase; this time, buy one free, one not free + shopping_cart3 = ShoppingCart.create user, jamtrack3, 1, true + + client.find_or_create_account(user, billing_info) + + sales = Sale.place_order(user, [shopping_cart3]) + + user.reload + user.sales.length.should eq(2) + sale = sales[0] + sale.reload + + 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 + + assert_free_line_item(sale.sale_line_items[0], jamtrack3) + end + it "for a free jam track with an affiliate association" do partner = FactoryGirl.create(:affiliate_partner) user.affiliate_referral = partner @@ -410,6 +569,295 @@ describe Sale do end end + describe "lessons" do + + + let(:teacher_user) { FactoryGirl.create(:teacher_user) } + let(:teacher) { teacher_user.teacher } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] } + let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } + let(:affiliate_partner) { FactoryGirl.create(:affiliate_partner) } + let(:affiliate_partner2) { FactoryGirl.create(:affiliate_partner, lesson_rate: 0.30) } + + describe "single" do + it "can succeed" do + + token = create_stripe_token + result = user.payment_update({token: token, zip: '72205', normal: true}) + + lesson_session = normal_lesson(user, teacher_user) + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + Timecop.travel(end_time + 1) + + # bill the user + LessonSession.hourly_check + + lesson_session.reload + payment = lesson_session.lesson_payment_charge + user.sales.count.should eql 1 + sale = user.sales.first + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should be 0 + sale.recurly_total_in_cents.should eql 3000 + sale.recurly_subtotal_in_cents.should eql 3000 + sale.recurly_currency.should eql 'USD' + line_item = sale.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.single.id + + user.reload + user.stripe_customer_id.should_not be nil + user.lesson_purchases.length.should eql 1 + user.remaining_test_drives.should eql 0 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 30.00 + lesson_purchase.lesson_package_type.is_normal?.should eql true + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + sale.sale_line_items.count.should eql 1 + line_item = sale.sale_line_items[0] + line_item.reload + line_item.affiliate_distributions.count.should eql 0 + end + + it "affiliate" do + user.affiliate_referral = affiliate_partner + user.save! + teacher_user.affiliate_referral = affiliate_partner2 + teacher_user.save! + + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', normal: true}) + + lesson_session = normal_lesson(user, teacher_user) + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + Timecop.travel(end_time + 1) + + # bill the user + LessonSession.hourly_check + + lesson_session.reload + payment = lesson_session.lesson_payment_charge + puts lesson_session.billing_error_reason + puts lesson_session.billing_error_detail + user.reload + user.sales.count.should eql 1 + sale = user.sales.first + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should be 248 + sale.recurly_total_in_cents.should eql 3248 + sale.recurly_subtotal_in_cents.should eql 3000 + sale.recurly_currency.should eql 'USD' + line_item = sale.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.single.id + + user.reload + user.stripe_customer_id.should_not be nil + user.lesson_purchases.length.should eql 1 + user.remaining_test_drives.should eql 0 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 30.00 + lesson_purchase.lesson_package_type.is_normal?.should eql true + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + sale.sale_line_items.count.should eql 1 + line_item = sale.sale_line_items[0] + line_item.reload + line_item.affiliate_distributions.count.should eql 2 + affiliate_partner.reload + affiliate_partner2.reload + affiliate_partner.affiliate_distributions.count.should eql 1 + affiliate_partner2.affiliate_distributions.count.should eql 1 + partner1_distribution = affiliate_partner.affiliate_distributions.first + partner2_distribution = affiliate_partner2.affiliate_distributions.first + partner1_distribution.sale_line_item.should eql partner2_distribution.sale_line_item + partner1_distribution.affiliate_referral_fee_in_cents.should eql (3000 * 0.25 * affiliate_partner.lesson_rate).round + partner2_distribution.affiliate_referral_fee_in_cents.should eql (3000 * 0.25 * affiliate_partner2.lesson_rate).round + end + + it "book recurring, monthly" do + + end + + end + describe "purchase_test_drive" do + it "can succeed" do + user.affiliate_referral = affiliate_partner + user.save! + teacher_user.affiliate_referral = affiliate_partner2 + teacher_user.save! + + + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_test_drive + + token = create_stripe_token + result = user.payment_update({token: token, zip: '72205', test_drive: true, booking_id: booking.id}) + + booking.reload + booking.card_presumed_ok.should be_true + + user.sales.count.should eql 1 + sale = result[:test_drive] + + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should be 0 + sale.recurly_total_in_cents.should eql 4999 + sale.recurly_subtotal_in_cents.should eql 4999 + sale.recurly_currency.should eql 'USD' + line_item = sale.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.test_drive_4.id + + user.reload + user.stripe_customer_id.should_not be nil + user.lesson_purchases.length.should eql 1 + user.remaining_test_drives.should eql 3 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 49.99 + lesson_purchase.lesson_package_type.is_test_drive?.should eql true + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + sale.sale_line_items.count.should eql 1 + line_item = sale.sale_line_items[0] + line_item.reload + line_item.affiliate_distributions.count.should eql 0 # test drives don't create affiliate + end + + it "can succeed with tax" do + #user.remaining_test_drives = 0 + #user.save! + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_test_drive + user.reload + user.remaining_test_drives.should eql 0 + + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', test_drive: true, booking_id: booking.id}) + + booking.reload + booking.card_presumed_ok.should be_true + + user.sales.count.should eql 1 + sale = result[:test_drive] + + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should be (4999 * 0.0825).round + sale.recurly_total_in_cents.should eql 4999 + (4999 * 0.0825).round + sale.recurly_subtotal_in_cents.should eql 4999 + sale.recurly_currency.should eql 'USD' + line_item = sale.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.test_drive_4.id + + + user.reload + user.stripe_customer_id.should_not be nil + user.lesson_purchases.length.should eql 1 + lesson_purchase = booking.lesson_sessions[0].lesson_package_purchase + user.remaining_test_drives.should eql 3 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 49.99 + lesson_purchase.lesson_package_type.is_test_drive?.should eql true + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + booking.lesson_sessions[0].accept({message: "got it", slot: booking.lesson_booking_slots[0].id}) + user.reload + user.remaining_test_drives.should eql 3 + line_item.affiliate_distributions.count.should eql 0 # test drives don't create affiliate + end + + it "can succeed with no booking; just intent" do + intent = TeacherIntent.create(user, teacher, 'book-test-drive') + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', test_drive: true}) + + user.errors.any?.should be_false + user.reload + user.has_stored_credit_card?.should be_true + + user.sales.count.should eql 1 + sale = result[:test_drive] + + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should be (4999 * 0.0825).round + sale.recurly_total_in_cents.should eql 4999 + (4999 * 0.0825).round + sale.recurly_subtotal_in_cents.should eql 4999 + sale.recurly_currency.should eql 'USD' + line_item = sale.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.test_drive_4.id + end + + it "will reject second test drive purchase" do + intent = TeacherIntent.create(user, teacher, 'book-test-drive') + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', test_drive: true}) + user.errors.any?.should be_false + user.reload + user.has_stored_credit_card?.should be_true + user.sales.count.should eql 1 + SaleLineItem.count.should eql 1 + Sale.count.should eql 1 + purchase = result[:purchase] + purchase.errors.any?.should be_false + + result = user.payment_update({token: token, zip: '78759', test_drive: true}) + user.errors.any?.should be_false + user.reload + user.has_stored_credit_card?.should be_true + user.sales.count.should eql 1 + purchase = result[:purchase] + purchase.errors.any?.should be_true + purchase.errors[:user].should eq ["can not buy test drive right now because you have already purchased it within the last year"] + SaleLineItem.count.should eql 1 + Sale.count.should eql 1 + + + end + end + end + describe "check_integrity_of_jam_track_sales" do let(:user) { FactoryGirl.create(:user) } diff --git a/ruby/spec/jam_ruby/models/school_invitation_spec.rb b/ruby/spec/jam_ruby/models/school_invitation_spec.rb new file mode 100644 index 000000000..b851d434f --- /dev/null +++ b/ruby/spec/jam_ruby/models/school_invitation_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe SchoolInvitation do + + let(:school) {FactoryGirl.create(:school)} + + it "created by factory" do + FactoryGirl.create(:school_invitation) + end + + it "created by method" do + SchoolInvitation.create(school.user, school, {as_teacher: true, first_name: "Bobby", last_name: "Jimes", email: "somewhere@jamkazam.com"}) + end + + describe "index" do + it "works" do + SchoolInvitation.index(school, {as_teacher: true})[:query].count.should eql 0 + + FactoryGirl.create(:school_invitation) + SchoolInvitation.index(school, {as_teacher: true})[:query].count.should eql 0 + SchoolInvitation.index(school, {as_teacher: false})[:query].count.should eql 0 + + FactoryGirl.create(:school_invitation, school: school, as_teacher: true) + SchoolInvitation.index(school, {as_teacher: true})[:query].count.should eql 1 + SchoolInvitation.index(school, {as_teacher: false})[:query].count.should eql 0 + + FactoryGirl.create(:school_invitation, school: school, as_teacher: false) + SchoolInvitation.index(school, {as_teacher: true})[:query].count.should eql 1 + SchoolInvitation.index(school, {as_teacher: false})[:query].count.should eql 1 + + end + + + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/school_spec.rb b/ruby/spec/jam_ruby/models/school_spec.rb new file mode 100644 index 000000000..5b8940c81 --- /dev/null +++ b/ruby/spec/jam_ruby/models/school_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe School do + + it "created by factory" do + FactoryGirl.create(:school) + end + + it "has correct associations" do + school = FactoryGirl.create(:school) + + school.should eql school.user.owned_school + + student = FactoryGirl.create(:user, school: school) + teacher = FactoryGirl.create(:teacher, school: school) + + school.reload + school.students.to_a.should eql [student] + school.teachers.to_a.should eql [teacher] + + student.school.should eql school + teacher.school.should eql school + end + + it "updates" do + school = FactoryGirl.create(:school) + school.update_from_params({name: 'hahah', scheduling_communication: 'school', correspondence_email: 'bobby@jamkazam.com'}) + school.errors.any?.should be_false + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/shopping_cart_spec.rb b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb index 6be02e8d4..8d3b724b1 100644 --- a/ruby/spec/jam_ruby/models/shopping_cart_spec.rb +++ b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb @@ -3,8 +3,15 @@ require 'spec_helper' describe ShoppingCart do let(:user) { FactoryGirl.create(:user) } - let(:jam_track) {FactoryGirl.create(:jam_track) } - let(:jam_track2) {FactoryGirl.create(:jam_track) } + let(:jam_track) { FactoryGirl.create(:jam_track) } + let(:jam_track2) { FactoryGirl.create(:jam_track) } + let(:jam_track3) { FactoryGirl.create(:jam_track) } + let(:jam_track4) { FactoryGirl.create(:jam_track) } + let(:jam_track5) { FactoryGirl.create(:jam_track) } + let(:jam_track6) { FactoryGirl.create(:jam_track) } + let(:jam_track7) { FactoryGirl.create(:jam_track) } + let(:gift_card) {FactoryGirl.create(:gift_card_type)} + let(:gift_card2) {FactoryGirl.create(:gift_card_type)} before(:each) do ShoppingCart.delete_all @@ -13,6 +20,9 @@ describe ShoppingCart do it "can reference a shopping cart" do shopping_cart = ShoppingCart.create user, jam_track, 1 + shopping_cart.errors.any?.should be_false + shopping_cart.valid?.should be_true + user.reload ShoppingCart.count.should == 1 user.shopping_carts.count.should == 1 user.shopping_carts[0].product_info[:name].should == jam_track.name @@ -21,18 +31,21 @@ describe ShoppingCart do user.shopping_carts[0].quantity.should == 1 end - - it "maintains only one fre JamTrack in ShoppingCart" do - cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track) + it "maintains only one free JamTrack in ShoppingCart" do + cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track, clear: true) cart1.should_not be_nil cart1.errors.any?.should be_false user.reload - cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track) + cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track, clear: true) 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 + cart3 = ShoppingCart.add_item_to_cart(user, gift_card) + cart3.errors.any?.should be_true + user.reload + user.shopping_carts.length.should eq(1) + cart4 = ShoppingCart.add_jam_track_to_cart(user, jam_track2, clear: true) + cart4.errors.any?.should be_false user.reload user.shopping_carts.length.should eq(1) end @@ -48,24 +61,159 @@ describe ShoppingCart do cart2.errors.any?.should be_true end + it "a second giftcard just adds quantity" do + + end + describe "redeemable behavior" 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, clear: true) + cart1.should_not be_nil + cart1.errors.any?.should be_false + cart1.marked_for_redeem.should eq(1) + user.reload + cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2, clear: true) + cart2.should_not be_nil + cart2.errors.any?.should be_false + cart2.marked_for_redeem.should eq(1) + + ShoppingCart.find_by_id(cart1.id).should be nil + + + ShoppingCart.remove_jam_track_from_cart(user, cart2) + + user.reload + user.shopping_carts.length.should eq(0) + ShoppingCart.find_by_id(cart2.id).should be nil + end + end + + describe "multiple free jamtracks" do + + before(:each) do + user.gifted_jamtracks = 5 + user.save! + end + + it "user can add and remove jamtracks without issue, until 'mixed' free/non-free is hit" 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_track2) - cart2.should_not be_nil - + cart2.errors.any?.should be_false + user.reload + user.shopping_carts.length.should eq(2) cart1.marked_for_redeem.should eq(1) cart2.marked_for_redeem.should eq(1) - ShoppingCart.remove_jam_track_from_cart(user, jam_track) - user.shopping_carts.length.should eq(0) - cart2.reload + cart3 = ShoppingCart.add_jam_track_to_cart(user, jam_track3) + cart3.errors.any?.should be_false + user.reload + user.shopping_carts.length.should eq(3) + cart1.marked_for_redeem.should eq(1) cart2.marked_for_redeem.should eq(1) + cart3.marked_for_redeem.should eq(1) + + cart4 = ShoppingCart.add_jam_track_to_cart(user, jam_track4) + cart4.errors.any?.should be_false + user.reload + user.shopping_carts.length.should eq(4) + cart1.marked_for_redeem.should eq(1) + cart2.marked_for_redeem.should eq(1) + cart3.marked_for_redeem.should eq(1) + cart4.marked_for_redeem.should eq(1) + + cart5 = ShoppingCart.add_jam_track_to_cart(user, jam_track5) + cart5.errors.any?.should be_false + user.reload + user.shopping_carts.length.should eq(5) + cart1.marked_for_redeem.should eq(1) + cart2.marked_for_redeem.should eq(1) + cart3.marked_for_redeem.should eq(1) + cart4.marked_for_redeem.should eq(1) + cart5.marked_for_redeem.should eq(1) + + cart6 = ShoppingCart.add_jam_track_to_cart(user, jam_track6) + cart6.errors.any?.should be_false + user.reload + user.shopping_carts.length.should eq(6) + cart1.marked_for_redeem.should eq(1) + cart2.marked_for_redeem.should eq(1) + cart3.marked_for_redeem.should eq(1) + cart4.marked_for_redeem.should eq(1) + cart5.marked_for_redeem.should eq(1) + cart6.marked_for_redeem.should eq(1) + + cart7 = ShoppingCart.add_jam_track_to_cart(user, jam_track7) + cart7.errors.any?.should be_true + user.reload + user.shopping_carts.length.should eq(6) + cart1.marked_for_redeem.should eq(1) + cart2.marked_for_redeem.should eq(1) + cart3.marked_for_redeem.should eq(1) + cart4.marked_for_redeem.should eq(1) + cart5.marked_for_redeem.should eq(1) + cart6.marked_for_redeem.should eq(1) + end + end + + describe "gift cards" do + it "can not add multiple of same type" do + cart1 = ShoppingCart.add_item_to_cart(user, gift_card) + cart1.should_not be_nil + cart1.errors.any?.should be_false + + user.reload + user.has_redeemable_jamtrack = true + user.shopping_carts.length.should eq(1) + user.shopping_carts[0].quantity.should eql(1) + + cart2 = ShoppingCart.add_item_to_cart(user, gift_card) + cart2.should_not be_nil + # it's the same type, so it's blocked + cart2.errors.any?.should be_true + cart2.errors[:cart_id].should eq(["has already been taken"]) + end + end + + describe "mixed" do + it "non-free then free" do + # you shouldn't be able to add a free after a non-free + 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.has_redeemable_jamtrack = true + user.save! + user.reload + cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2) + cart2.errors.any?.should be_true + cart2.errors[:base].should eq(["You can not add a free JamTrack to a cart with non-free items. Please clear out your cart."]) + + user.shopping_carts.length.should eq(1) + end + + it "free then non-free" 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_track2) + cart2.errors.any?.should be_true + cart2.errors[:base].should eq(["You can not add a non-free JamTrack to a cart containing free items. Please clear out your cart."]) + + user.shopping_carts.length.should eq(1) end end end diff --git a/ruby/spec/jam_ruby/models/teacher_distribution_spec.rb b/ruby/spec/jam_ruby/models/teacher_distribution_spec.rb new file mode 100644 index 000000000..dbf54d562 --- /dev/null +++ b/ruby/spec/jam_ruby/models/teacher_distribution_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe TeacherDistribution do + + let(:teacher) {FactoryGirl.create(:teacher_user)} + + + describe "index" do + it "empty" do + TeacherDistribution.index(teacher, {})[:query].count.should eql 0 + end + + it "returns single" do + distribution = FactoryGirl.create(:teacher_distribution, teacher: teacher) + + TeacherDistribution.index(teacher, {})[:query].count.should eql 1 + + distribution = FactoryGirl.create(:teacher_distribution) # some random teacher + + TeacherDistribution.index(teacher, {})[:query].count.should eql 1 + end + end +end diff --git a/ruby/spec/jam_ruby/models/teacher_payment_spec.rb b/ruby/spec/jam_ruby/models/teacher_payment_spec.rb new file mode 100644 index 000000000..446d9e01e --- /dev/null +++ b/ruby/spec/jam_ruby/models/teacher_payment_spec.rb @@ -0,0 +1,491 @@ +require 'spec_helper' + +describe TeacherPayment do + + let(:user) { FactoryGirl.create(:user) } + let(:user2) { FactoryGirl.create(:user) } + let(:teacher1_auth) { UserAuthorization.create(provider: 'stripe_connect', uid: stripe_account1_id, token: 'abc', refresh_token: 'abc', token_expiration: Date.today + 365, secret: 'secret') } + let(:teacher2_auth) { UserAuthorization.create(provider: 'stripe_connect', uid: stripe_account2_id, token: 'abc', refresh_token: 'abc', token_expiration: Date.today + 365, secret: 'secret') } + let(:teacher) { FactoryGirl.create(:user) } + let(:teacher2) { FactoryGirl.create(:user) } + let(:teacher_obj) { FactoryGirl.create(:teacher, user: teacher) } + let(:teacher_obj2) { FactoryGirl.create(:teacher, user: teacher2) } + + let(:school_teacher) { FactoryGirl.create(:user) } + let(:school_owner_teacher) { FactoryGirl.create(:teacher, user: school_teacher) } + + let(:test_drive_lesson) { testdrive_lesson(user, teacher) } + let(:test_drive_lesson2) { testdrive_lesson(user2, teacher2) } + let(:test_drive_distribution) { FactoryGirl.create(:teacher_distribution, lesson_session: test_drive_lesson, teacher: teacher, teacher_payment: nil, ready: false) } + let(:test_drive_distribution2) { FactoryGirl.create(:teacher_distribution, lesson_session: test_drive_lesson2, teacher: teacher2, teacher_payment: nil, ready: false) } + let(:normal_lesson_session) { normal_lesson(user, teacher) } + let(:normal_distribution) { FactoryGirl.create(:teacher_distribution, lesson_session: normal_lesson_session, teacher: teacher, teacher_payment: nil, ready: false) } + let(:school) { FactoryGirl.create(:school, user: school_teacher) } + + describe "pending_teacher_payments" do + + describe "normal teachers" do + before(:each) do + teacher_obj.touch + teacher_obj2.touch + teacher.teacher.stripe_account_id = stripe_account1_id + teacher2.teacher.stripe_account_id = stripe_account2_id + end + it "empty" do + TeacherPayment.pending_teacher_payments.count.should eql 0 + end + + it "one distribution" do + teacher.should eql teacher_obj.user + test_drive_distribution.touch + + teacher.user_authorizations.count.should eql 1 + user_auth = teacher.user_authorizations[0] + user_auth.provider.should eql 'stripe_connect' + payments = TeacherPayment.pending_teacher_payments + payments.count.should eql 0 + + test_drive_distribution.ready = true + test_drive_distribution.save! + test_drive_distribution.distributed.should be_false + + payments = TeacherPayment.pending_teacher_payments + payments.count.should eql 1 + payments[0]['id'].should eql teacher.id + end + + it "multiple teachers" do + test_drive_distribution.touch + test_drive_distribution2.touch + + payments = TeacherPayment.pending_teacher_payments + payments.count.should eql 0 + + test_drive_distribution.ready = true + test_drive_distribution.save! + test_drive_distribution2.ready = true + test_drive_distribution2.save! + + payments = TeacherPayment.pending_teacher_payments + payments.count.should eql 2 + payment_user_ids = payments.map(&:id) + payment_user_ids.include? teacher.id + payment_user_ids.include? teacher2.id + end + end + + describe "school teachers" do + before(:each) do + teacher_obj.touch + teacher.teacher.stripe_account_id = stripe_account1_id + end + + it "school distribution" do + + test_drive_distribution.school = school + test_drive_distribution.save! + + payments = TeacherPayment.pending_teacher_payments + payments.count.should eql 0 + + test_drive_distribution.ready = true + test_drive_distribution.save! + + payments = TeacherPayment.pending_teacher_payments + payments.count.should eql 1 + payments[0]['id'].should eql teacher.id + end + + end + end + + describe "teacher_payments" do + + describe "normal teachers" do + before(:each) do + teacher_obj.touch + teacher_obj2.touch + teacher.teacher.stripe_account_id = stripe_account1_id + teacher2.teacher.stripe_account_id = stripe_account2_id + end + + + it "empty" do + TeacherPayment.teacher_payments + end + + it "charges test drive" do + test_drive_distribution.touch + + test_drive_distribution.ready = true + test_drive_distribution.save! + + TeacherPayment.teacher_payments + + test_drive_distribution.reload + test_drive_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 1 + + payment = test_drive_distribution.teacher_payment + + if payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_detail + end + payment.teacher_payment_charge.billed.should eql true + payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql 0 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) + charge.amount.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + charge.application_fee.should eql nil + + TeacherPayment.pending_teacher_payments.count.should eql 0 + end + + it "charges normal" do + normal_distribution.touch + + normal_distribution.ready = true + normal_distribution.save! + UserMailer.deliveries.clear + + TeacherPayment.teacher_payments + + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 1 + + payment = normal_distribution.teacher_payment + + if payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_detail + end + + # only one confirm email to teacher + UserMailer.deliveries.length.should eql 1 + payment.teacher_payment_charge.billed.should eql true + payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql 280 + payment.teacher_payment_charge.teacher.should eql teacher + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) + charge.amount.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + charge.application_fee.should include("fee_") + end + + + it "charges multiple" do + test_drive_distribution.touch + test_drive_distribution.ready = true + test_drive_distribution.save! + normal_distribution.touch + normal_distribution.ready = true + normal_distribution.save! + + TeacherPayment.teacher_payments + + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 2 + + payment = normal_distribution.teacher_payment + + if payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_detail + end + payment.teacher_payment_charge.billed.should eql true + payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql 280 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) + charge.amount.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + charge.application_fee.should include("fee_") + + test_drive_distribution.reload + payment = test_drive_distribution.teacher_payment + + if payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_detail + end + payment.teacher_payment_charge.billed.should eql true + payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql 0 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) + charge.amount.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + charge.application_fee.should be_nil + end + + + end + + describe "school teachers" do + before(:each) do + teacher_obj.touch + teacher.teacher.stripe_account_id = stripe_account1_id + school_owner_teacher.touch + school_teacher.teacher.stripe_account_id = stripe_account2_id + end + + + it "charges school" do + teacher.touch + normal_distribution.school = school + normal_distribution.ready = true + normal_distribution.save! + + UserMailer.deliveries.clear + TeacherPayment.teacher_payments + + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + normal_distribution.teacher_payment.school.should eql school + TeacherPayment.count.should eql 1 + + payment = normal_distribution.teacher_payment + + if payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_detail + end + # one to school owner, one to teacher + UserMailer.deliveries.length.should eql 2 + payment.teacher_payment_charge.billed.should eql true + payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql 280 + payment.teacher_payment_charge.user.should eql school.owner + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) + charge.destination.should eql school.owner.teacher.stripe_account_id + charge.amount.should eql 1008 + charge.application_fee.should include("fee_") + end + end + + describe "stripe mocked" do + before { + StripeMock.start + } + + after { StripeMock.stop } + describe "normal" do + before { + teacher_obj.touch + teacher_obj2.touch + teacher.teacher.stripe_account_id = stripe_account1_id + teacher2.teacher.stripe_account_id = stripe_account2_id + } + + it "failed payment, then success" do + StripeMock.prepare_card_error(:card_declined) + + normal_distribution.touch + normal_distribution.ready = true + normal_distribution.save! + + TeacherPayment.teacher_payments + + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 1 + + payment = normal_distribution.teacher_payment + + payment.teacher_payment_charge.billing_error_reason.should eql("card_declined") + payment.teacher_payment_charge.billing_error_detail.should include("declined") + + payment.teacher_payment_charge.billed.should eql false + payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql 280 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + + payment.teacher_payment_charge.stripe_charge_id.should be_nil + + StripeMock.clear_errors + + TeacherPayment.teacher_payments + + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 1 + + # make sure the teacher_payment is reused, and charge is reused + normal_distribution.teacher_payment.should eql(payment) + normal_distribution.teacher_payment.teacher_payment_charge.should eql(payment.teacher_payment_charge) + + # no attempt should be made because a day hasn't gone by + payment = normal_distribution.teacher_payment + payment.teacher_payment_charge.billed.should eql false + payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql 280 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + + # advance one day so that a charge is attempted again + Timecop.freeze(Date.today + 2) + + TeacherPayment.teacher_payments + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 1 + + # make sure the teacher_payment is reused, and charge is reused + normal_distribution.teacher_payment.should eql(payment) + normal_distribution.teacher_payment.teacher_payment_charge.should eql(payment.teacher_payment_charge) + + # no attempt should be made because a day hasn't gone by + payment = normal_distribution.teacher_payment + payment.reload + payment.teacher_payment_charge.billed.should eql true + payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql 280 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) + charge.amount.should eql 1008 + end + + + + it "charges multiple (with initial failure)" do + StripeMock.prepare_card_error(:card_declined) + + test_drive_distribution.touch + test_drive_distribution.ready = true + test_drive_distribution.save! + normal_distribution.touch + normal_distribution.ready = true + normal_distribution.save! + + TeacherPayment.teacher_payments + + TeacherPayment.count.should eql 1 + payment = TeacherPayment.first + payment.teacher_payment_charge.billed.should be_false + + # advance one day so that a charge is attempted again + Timecop.freeze(Date.today + 2) + + StripeMock.clear_errors + TeacherPayment.teacher_payments + + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 2 + + payment = normal_distribution.teacher_payment + + payment.teacher_payment_charge.billed.should eql true + payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql 280 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) + charge.amount.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + + test_drive_distribution.reload + payment = test_drive_distribution.teacher_payment + + payment.teacher_payment_charge.billed.should eql true + payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql 0 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) + charge.amount.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + end + end + + describe "school" do + before { + teacher_obj.touch + teacher.teacher.stripe_account_id = stripe_account1_id + school_owner_teacher.touch + school_teacher.teacher.stripe_account_id = stripe_account2_id + } + + + it "failed payment, then success (school)" do + StripeMock.prepare_card_error(:card_declined) + + normal_distribution.school = school + normal_distribution.ready = true + normal_distribution.save! + + TeacherPayment.teacher_payments + + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 1 + + payment = normal_distribution.teacher_payment + + payment.teacher_payment_charge.billing_error_reason.should eql("card_declined") + payment.teacher_payment_charge.billing_error_detail.should include("declined") + + payment.teacher_payment_charge.billed.should eql false + payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql 280 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + + payment.teacher_payment_charge.stripe_charge_id.should be_nil + + StripeMock.clear_errors + + TeacherPayment.teacher_payments + + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 1 + + # make sure the teacher_payment is reused, and charge is reused + normal_distribution.teacher_payment.should eql(payment) + normal_distribution.teacher_payment.teacher_payment_charge.should eql(payment.teacher_payment_charge) + + # no attempt should be made because a day hasn't gone by + payment = normal_distribution.teacher_payment + payment.teacher_payment_charge.billed.should eql false + payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql 280 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + + # advance one day so that a charge is attempted again + Timecop.freeze(Date.today + 2) + + TeacherPayment.teacher_payments + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 1 + + # make sure the teacher_payment is reused, and charge is reused + normal_distribution.teacher_payment.should eql(payment) + normal_distribution.teacher_payment.teacher_payment_charge.should eql(payment.teacher_payment_charge) + + # no attempt should be made because a day hasn't gone by + payment = normal_distribution.teacher_payment + payment.reload + payment.teacher_payment_charge.billed.should eql true + payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql 280 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) + charge.amount.should eql 1008 + end + end + end + + end +end diff --git a/ruby/spec/jam_ruby/models/teacher_spec.rb b/ruby/spec/jam_ruby/models/teacher_spec.rb new file mode 100644 index 000000000..b5aefc5e5 --- /dev/null +++ b/ruby/spec/jam_ruby/models/teacher_spec.rb @@ -0,0 +1,485 @@ +require 'spec_helper' + +describe Teacher do + + let(:user) { FactoryGirl.create(:user) } + let(:genre1) { FactoryGirl.create(:genre) } + let(:genre2) { FactoryGirl.create(:genre) } + let(:subject1) { FactoryGirl.create(:subject) } + let(:subject2) { FactoryGirl.create(:subject) } + let(:language1) { FactoryGirl.create(:language) } + let(:language2) { FactoryGirl.create(:language) } + let(:instrument1) { FactoryGirl.create(:instrument, :description => 'a great instrument')} + let(:instrument2) { FactoryGirl.create(:instrument, :description => 'an ok instrument')} + + describe "index" do + it "no params" do + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) + teachers = Teacher.index(nil)[:query] + teachers.length.should eq 1 + teachers[0].should eq(teacher.user) + end + + it "sorting" do + + teacher3 = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) + teacher3.user.email = 'phantom+dogpound@jamkazam.com' + teacher3.user.save! + + + teacher2 = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) + teacher2.top_rated = false + teacher2.save! + + + teacher1 = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) + teacher1.top_rated = true + teacher1.save! + + query = Teacher.index(nil, {})[:query] + query.count.should eql 3 + query[0].should eq(teacher1.user) + query[1].should eq(teacher2.user) + query[2].should eq(teacher3.user) + end + + it "instruments" do + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) + #teachers = Teacher.index(nil, {instruments: ['acoustic guitar']})[:query] + #teachers.length.should eq 0 + + teacher.instruments << Instrument.find('acoustic guitar') + teacher.save! + #teachers = Teacher.index(nil, {instruments: ['acoustic guitar']})[:query] + #teachers.length.should eq 1 + #teachers[0].should eq(teacher.user) + + #teacher.instruments << Instrument.find('electric guitar') + #teacher.save! + puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + teachers = Teacher.index(nil, {instruments: ['acoustic guitar']})[:query] + puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!---" + teachers.length.should eq 1 + teachers[0].should eq(teacher.user) + end + + it "subjects" do + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) + teachers = Teacher.index(nil, {subjects: ['music-theory']})[:query] + teachers.length.should eq 0 + + teacher.subjects << Subject.find('music-theory') + teacher.save! + teachers = Teacher.index(nil, {subjects: ['music-theory']})[:query] + teachers.length.should eq 1 + teachers[0].should eq(teacher.user) + end + + it "genres" do + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) + teachers = Teacher.index(nil, {genres: ['ambient']})[:query] + teachers.length.should eq 0 + + teacher.genres << Genre.find('ambient') + teacher.save! + teachers = Teacher.index(nil, {genres: ['ambient']})[:query] + teachers.length.should eq 1 + teachers[0].should eq(teacher.user) + end + + + it "languages" do + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) + teachers = Teacher.index(nil, {languages: ['EN']})[:query] + teachers.length.should eq 0 + + teacher.languages << Language.find('EN') + teacher.save! + teachers = Teacher.index(nil, {languages: ['EN']})[:query] + teachers.length.should eq 1 + teachers[0].should eq(teacher.user) + end + + it "country" do + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) + teachers = Teacher.index(nil, {country: 'DO'})[:query] + teachers.length.should eq 0 + + teacher.user.country = 'DO' + teacher.user.save! + teachers = Teacher.index(nil, {country: 'DO'})[:query] + teachers.length.should eq 1 + teachers[0].should eq(teacher.user) + end + + it "region" do + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) + teachers = Teacher.index(nil, {region: 'HE'})[:query] + teachers.length.should eq 0 + + teacher.user.state = 'HE' + teacher.user.save! + teachers = Teacher.index(nil, {region: 'HE'})[:query] + teachers.length.should eq 1 + teachers[0].should eq(teacher.user) + end + + it "years_teaching" do + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now, years_teaching: 5) + teachers = Teacher.index(nil, {years_teaching: 10})[:query] + teachers.length.should eq 0 + + teachers = Teacher.index(nil, {years_teaching: 2})[:query] + teachers.length.should eq 1 + teachers[0].should eq(teacher.user) + end + + it "teaches beginner/intermediate/advanced" do + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) + teachers = Teacher.index(nil, {teaches_beginner: true})[:query] + teachers.length.should eq 0 + + teacher.teaches_beginner = true + teacher.save! + teachers = Teacher.index(nil, {teaches_beginner: true})[:query] + teachers.length.should eq 1 + teachers[0].should eq(teacher.user) + + teachers = Teacher.index(nil, {teaches_intermediate: true})[:query] + teachers.length.should eq 0 + + teachers = Teacher.index(nil, {teaches_beginner: true, teaches_intermediate: true})[:query] + teachers.length.should eq 1 + teachers[0].should eq(teacher.user) + + teachers = Teacher.index(nil, {teaches_beginner: true, teaches_intermediate: true, teaches_advanced: true})[:query] + teachers.length.should eq 1 + teachers[0].should eq(teacher.user) + end + + it "student_age" do + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) + teachers = Teacher.index(nil, {student_age: 5})[:query] + teachers.length.should eq 1 + + teacher.teaches_age_lower = 5 + teacher.save! + teachers = Teacher.index(nil, {student_age: 5})[:query] + teachers.length.should eq 1 + teachers[0].should eq(teacher.user) + + teacher.teaches_age_lower = 6 + teacher.save! + teachers = Teacher.index(nil, {student_age: 5})[:query] + teachers.length.should eq 0 + + teacher.teaches_age_lower = 4 + teacher.teaches_age_upper = 6 + teacher.save! + teachers = Teacher.index(nil, {student_age: 5})[:query] + teachers.length.should eq 1 + teachers[0].should eq(teacher.user) + + teacher.teaches_age_lower = 0 + teacher.teaches_age_upper = 5 + teacher.save! + teachers = Teacher.index(nil, {student_age: 5})[:query] + teachers.length.should eq 1 + teachers[0].should eq(teacher.user) + + teacher.teaches_age_lower = 0 + teacher.teaches_age_upper = 6 + teacher.save! + teachers = Teacher.index(nil, {student_age: 5})[:query] + teachers.length.should eq 1 + teachers[0].should eq(teacher.user) + + teacher.teaches_age_lower = 0 + teacher.teaches_age_upper = 4 + teacher.save! + teachers = Teacher.index(nil, {student_age: 5})[:query] + teachers.length.should eq 0 + end + end + + BIO = "Once a man learned a guitar." + GOOD_YOUTUBE_URL = "http://youtube.com/watch?v=1234567890" + describe "can create" do + it "a simple teacher" do + teacher = Teacher.new + teacher.user = user + teacher.biography = BIO + teacher.introductory_video = GOOD_YOUTUBE_URL + teacher.save.should be_true + t = Teacher.find(teacher.id) + t.biography.should == BIO + t.introductory_video.should == GOOD_YOUTUBE_URL + end + + + it "with instruments" do + teacher = Teacher.build_teacher(user, {}) + teacher.save + teacher.instruments << instrument1 + teacher.instruments << instrument2 + teacher.save.should be_true + t = Teacher.find(teacher.id) + t.instruments.should have(2).items + end + + end + + describe "using save_teacher can create" do + it "introduction" do + teacher = Teacher.save_teacher( + user, + biography: BIO, + introductory_video: GOOD_YOUTUBE_URL, + years_teaching: 21, + years_playing: 12 + ) + teacher.should_not be_nil + teacher.user.should eql user + teacher.errors.should be_empty + teacher.id.should_not be_nil + t = Teacher.find(teacher.id) + t.biography.should == BIO + t.introductory_video.should == GOOD_YOUTUBE_URL + t.years_teaching.should == 21 + t.years_playing.should == 12 + end + + it "basics" do + teacher_user = FactoryGirl.create(:teacher_user) + teacher = Teacher.save_teacher( + teacher_user, + instruments: [instrument1.id, instrument2.id], + subjects: [subject1.id, subject2.id], + genres: [genre1.id, genre2.id], + languages: [language1.id, language2.id], + teaches_age_lower: 10, + teaches_age_upper: 20, + teaches_beginner: true, + teaches_intermediate: false, + teaches_advanced: true + ) + + teacher.should_not be_nil + teacher.errors.should be_empty + + t = Teacher.find(teacher.id) + + # Instruments + t.instruments.should have(2).items + + # Genres + t.genres.should have(2).items + + # Subjects + t.subjects.should have(2).items + + # Languages + t.languages.should have(2).items + + t.teaches_age_lower.should == 10 + t.teaches_age_upper.should == 20 + t.teaches_beginner.should be_true + t.teaches_intermediate.should be_false + t.teaches_advanced.should be_true + + end + + it "experience" do + experience = [{ + name: "Professor", + organization: "SHSU", + start_year: 1994, + end_year: 2004 + } + ] + + teacher = Teacher.save_teacher(user, experiences_teaching: experience) + teacher.should_not be_nil + teacher.errors.should be_empty + + + t = Teacher.find(teacher.id) + t.should_not be_nil + + + t.teacher_experiences.should have(1).items + t.experiences_teaching.should have(1).items + t.experiences_education.should have(0).items + t.experiences_award.should have(0).items + + # Save some awards and re-check teacher object: + teacher = Teacher.save_teacher(user, experiences_award: experience) + teacher.should_not be_nil + teacher.errors.should be_empty + + t.reload + + t.teacher_experiences.should have(2).items + t.experiences_teaching.should have(1).items + t.experiences_education.should have(0).items + t.experiences_award.should have(1).items + + + end + + it "lesson pricing" do + teacher = Teacher.save_teacher( + user, + prices_per_lesson: true, + prices_per_month: true, + lesson_duration_30: true, + lesson_duration_45: true, + lesson_duration_60: true, + lesson_duration_90: true, + lesson_duration_120: true, + price_per_lesson_30_cents: 3000, + price_per_lesson_45_cents: 3000, + price_per_lesson_60_cents: 3000, + price_per_lesson_90_cents: 3000, + price_per_lesson_120_cents: 3000, + price_per_month_30_cents: 5000, + price_per_month_45_cents: 5000, + price_per_month_60_cents: 5000, + price_per_month_90_cents: 5000, + price_per_month_120_cents: 5000 + ) + + teacher.should_not be_nil + teacher.id.should_not be_nil + teacher.errors.should be_empty + + t = Teacher.find(teacher.id) + t.prices_per_lesson.should be_true + t.prices_per_month.should be_true + t.lesson_duration_30.should be_true + t.lesson_duration_45.should be_true + t.lesson_duration_60.should be_true + t.lesson_duration_90.should be_true + t.lesson_duration_120.should be_true + t.price_per_lesson_30_cents.should == 3000 + t.price_per_lesson_45_cents.should == 3000 + t.price_per_lesson_60_cents.should == 3000 + t.price_per_lesson_90_cents.should == 3000 + t.price_per_lesson_120_cents.should == 3000 + t.price_per_month_30_cents.should == 5000 + t.price_per_month_45_cents.should == 5000 + t.price_per_month_60_cents.should == 5000 + t.price_per_month_90_cents.should == 5000 + t.price_per_month_120_cents.should == 5000 + end + end + + describe "validates" do + it "barebones" do + teacher = Teacher.save_teacher(user, validate_introduction: true, biography: "Teaches for dat school") + teacher.errors.should be_empty + + end + it "introduction" do + teacher = Teacher.save_teacher( + user, + years_teaching: 21, + validate_introduction: true + ) + + teacher.should_not be_nil + teacher.id.should be_nil + teacher.errors.should_not be_empty + + teacher.errors.should have_key(:biography) + end + + it "introductory video" do + teacher = Teacher.save_teacher( + user, + biography: BIO, + introductory_video: "fubar.com/nothing", + validate_introduction: true + ) + + teacher.should_not be_nil + teacher.id.should be_nil + teacher.errors.should_not be_empty + + teacher.errors.should have_key(:introductory_video) + + user.reload + teacher = Teacher.save_teacher( + user, + biography: BIO, + introductory_video: GOOD_YOUTUBE_URL, + validate_introduction: true + ) + + puts "teacher.errors #{teacher.errors.inspect}" + teacher.should_not be_nil + teacher.id.should_not be_nil + teacher.errors.should be_empty + + end + + it "basics" do + teacher = Teacher.save_teacher( + user, + # instruments: [instrument1, instrument2], + # subjects: [subject1, subject2], + # genres: [genre1, genre2], + # languages: [language1, language2], + teaches_age_lower: 10, + teaches_beginner: true, + teaches_intermediate: false, + teaches_advanced: true, + validate_basics: true + ) + + teacher.should_not be_nil + teacher.id.should be_nil + teacher.errors.should have_key(:instruments) + teacher.errors.should have_key(:subjects) + teacher.errors.should have_key(:genres) + teacher.errors.should have_key(:languages) + end + + it "pricing" do + teacher = Teacher.save_teacher( + user, + prices_per_lesson: false, + prices_per_month: false, + lesson_duration_30: false, + lesson_duration_45: false, + lesson_duration_60: false, + lesson_duration_90: false, + lesson_duration_120: false, + #price_per_lesson_30_cents: 3000, + price_per_lesson_45_cents: 3000, + #price_per_lesson_60_cents: 3000, + #price_per_lesson_90_cents: 3000, + price_per_lesson_120_cents: 3000, + validate_pricing:true + ) + + teacher.should_not be_nil + teacher.id.should be_nil + teacher.errors.should_not be_empty + teacher.errors.should have_key(:offer_pricing) + teacher.errors.should have_key(:offer_duration) + + teacher = Teacher.save_teacher( + user, + prices_per_month: true, + lesson_duration_45: true, + validate_pricing:true + ) + + teacher.should_not be_nil + teacher.id.should_not be_nil + teacher.errors.should be_empty + + end # pricing + end # validates +end # spec diff --git a/ruby/spec/jam_ruby/models/test_drive_package_choice_spec.rb b/ruby/spec/jam_ruby/models/test_drive_package_choice_spec.rb new file mode 100644 index 000000000..9e7eb4de4 --- /dev/null +++ b/ruby/spec/jam_ruby/models/test_drive_package_choice_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe TestDrivePackageChoice do + + it "works" do + + choice = FactoryGirl.create(:test_drive_package_choice) + choice.test_drive_package.package_type.should eql '4' + choice.test_drive_package.test_drive_package_teachers.count.should eql 4 + + teacher_choice = FactoryGirl.create(:test_drive_package_choice_teacher, test_drive_package_choice: choice) + teacher_choice.test_drive_package_choice.should eql choice + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/user_blacklist_spec.rb b/ruby/spec/jam_ruby/models/user_blacklist_spec.rb new file mode 100644 index 000000000..48668d123 --- /dev/null +++ b/ruby/spec/jam_ruby/models/user_blacklist_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe UserBlacklist do + + let(:user) { FactoryGirl.create(:user) } + + describe "#banned" do + it "returns false if no ban" do + UserBlacklist.banned(user).should eq false + end + + it "returns true if banned" do + FactoryGirl.create(:user_blacklist, user: user) + UserBlacklist.banned(user).should eq true + end + + it "returns false if whitelisted" do + FactoryGirl.create(:user_whitelist, user: user) + UserBlacklist.banned(user).should eq false + end + + + it "returns false if whitelisted and blacklisted too" do + FactoryGirl.create(:user_blacklist, user: user) + FactoryGirl.create(:user_whitelist, user: user) + UserBlacklist.banned(user).should eq false + end + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/user_spec.rb b/ruby/spec/jam_ruby/models/user_spec.rb index e317f54e2..c74e78f49 100644 --- a/ruby/spec/jam_ruby/models/user_spec.rb +++ b/ruby/spec/jam_ruby/models/user_spec.rb @@ -97,16 +97,16 @@ describe User do it { should be_admin } end - + describe "when first name is not present" do before { @user.first_name = " " } - it { should_not be_valid } + it { should be_valid } end describe "when last name is not present" do before { @user.last_name = " " } - it { should_not be_valid } + it { should be_valid } end describe "when email is not present" do @@ -136,14 +136,13 @@ describe User do end - describe "first or last name cant have profanity" do it "should not let the first name have profanity" do @user.first_name = "fuck you" @user.save @user.should_not be_valid end - + it "should not let the last name have profanity" do @user.last_name = "fuck you" @user.save @@ -226,7 +225,7 @@ describe User do User.authenticate(@user.email, "newpassword").should_not be_nil UserMailer.deliveries.length.should == 1 end - + it "setting a new password should fail if old one doesnt match" do @user.set_password("wrongold", "newpassword", "newpassword") @user.errors.any?.should be_true @@ -240,7 +239,7 @@ describe User do @user.errors[:password_confirmation].length.should == 1 UserMailer.deliveries.length.should == 0 end - + it "setting a new password should fail if new one doesnt validate" do @user.set_password("foobar", "a", "a") @user.errors.any?.should be_true @@ -255,15 +254,15 @@ describe User do @user.errors[:password_confirmation].length.should == 1 UserMailer.deliveries.length.should == 0 end - + end - + describe "reset_password" do before do @user.confirm_email! @user.save end - + it "fails if the provided email address is unrecognized" do expect { User.reset_password("invalidemail@invalid.com", RESET_PASSWORD_URL) }.to raise_error(JamRuby::JamArgumentError) end @@ -332,7 +331,7 @@ describe User do describe "authenticate (class-instance)" do before { @user.email_confirmed=true; @user.save } - + describe "with valid password" do it { should == User.authenticate(@user.email, @user.password) } end @@ -355,7 +354,7 @@ describe User do end describe "create_dev_user" do - before { @dev_user = User.create_dev_user("Seth", "Call", "seth@jamkazam.com", "Jam123", "Austin", "Texas", "US", nil, nil) } + before { @dev_user = User.create_dev_user("Seth", "Call", "seth@jamkazam.com", "Jam123", "Austin", "Texas", "US", nil, nil) } subject { @dev_user } @@ -366,7 +365,7 @@ describe User do describe "should not be a new record" do it { should be_persisted } end - + describe "updates record" do before { @dev_user = User.create_dev_user("Seth", "Call2", "seth@jamkazam.com", "Jam123", "Austin", "Texas", "US", nil, nil) } @@ -471,22 +470,22 @@ describe User do describe "finalize email updates recurly" do before do - + @user.begin_update_email("somenewemail@blah.com", "foobar", "http://www.jamkazam.com/confirm_email_update?token=") UserMailer.deliveries.clear billing_info = { - first_name: @user.first_name, - last_name: @user.last_name, - address1: 'Test Address 1', - address2: 'Test Address 2', - city: @user.city, - state: @user.state, - country: @user.country, - zip: '12345', - number: '4111-1111-1111-1111', - month: '08', - year: '2017', - verification_value: '111' + first_name: @user.first_name, + last_name: @user.last_name, + address1: 'Test Address 1', + address2: 'Test Address 2', + city: @user.city, + state: @user.state, + country: @user.country, + zip: '12345', + number: '4111-1111-1111-1111', + month: '08', + year: '2017', + verification_value: '111' } @recurly.find_or_create_account(@user, billing_info) end @@ -496,30 +495,30 @@ describe User do @recurly.get_account(@user).email.should_not == "somenewemail@blah.com" @finalized = User.finalize_update_email(@user.update_email_token) @recurly.get_account(@user).email.should == "somenewemail@blah.com" - end + end end describe "user_authorizations" do it "can create" do - @user.user_authorizations.build provider: 'facebook', - uid: '1', - token: '1', - token_expiration: Time.now, - user: @user + @user.user_authorizations.build provider: 'facebook', + uid: '1', + token: '1', + token_expiration: Time.now, + user: @user @user.save! end it "fails on duplicate" do - @user.user_authorizations.build provider: 'facebook', - uid: '1', - token: '1', - token_expiration: Time.now, - user: @user + @user.user_authorizations.build provider: 'facebook', + uid: '1', + token: '1', + token_expiration: Time.now, + user: @user @user.save! @user2 = FactoryGirl.create(:user) - @user2.user_authorizations.build provider: 'facebook', + @user2.user_authorizations.build provider: 'facebook', uid: '1', token: '1', token_expiration: Time.now, @@ -533,7 +532,7 @@ describe User do describe "mods" do it "should allow update of JSON" do - @user.mods = {no_show: {something:1}}.to_json + @user.mods = {no_show: {something: 1}}.to_json @user.save! end end @@ -624,7 +623,7 @@ describe User do end it "remains null if the user's last_jam_addr is null" do - @user.last_jam_addr.should be_nil # make sure the factory still makes a null addr to start + @user.last_jam_addr.should be_nil # make sure the factory still makes a null addr to start User.update_locidispids(false) @user.reload @user.last_jam_addr.should be_nil @@ -701,7 +700,7 @@ describe User do end describe "age" do - let(:user) {FactoryGirl.create(:user)} + let(:user) { FactoryGirl.create(:user) } it "should calculate age based on birth_date" do user.birth_date = Time.now - 10.years @@ -716,7 +715,7 @@ describe User do end describe "mods_merge" do - let(:user) {FactoryGirl.create(:user)} + let(:user) { FactoryGirl.create(:user) } it "allow empty merge" do user.mod_merge({}) @@ -728,6 +727,7 @@ describe User do user.mod_merge({"no_show" => {"some_screen" => true}}) user.valid?.should be_true user.mods.should == {"no_show" => {"some_screen" => true}} + end it "allow no_show aggregation" do @@ -749,18 +749,188 @@ describe User do end it "does not allow random root keys" do - user.mod_merge({random_root_key:true}) + user.mod_merge({random_root_key: true}) user.valid?.should be_false user.errors[:mods].should == [ValidationMessages::MODS_UNKNOWN_KEY] end it "does not allow non-hash no_show" do - user.mod_merge({no_show:true}) + user.mod_merge({no_show: true}) user.valid?.should be_false user.errors[:mods].should == [ValidationMessages::MODS_MUST_BE_HASH] end end + describe "sync_stripe_customer" do + let(:user) { FactoryGirl.create(:user) } + let(:token1) { create_stripe_token } + let(:token2) { create_stripe_token(2018) } + + # possible Stripe::InvalidRequestError + it "reuses user on card update" do + user.stripe_customer_id.should be_nil + user.payment_update({stripe_token: token1}) + user.reload + user.stripe_customer_id.should_not be_nil + customer1 = user.stripe_customer_id + + # let's change email address too + user.email = 'unique+1@jamkazam.com' + user.save! + + token2.should_not eql token1 + user.payment_update({stripe_token: token2}) + user.reload + user.stripe_customer_id.should_not be_nil + customer2 = user.stripe_customer_id + + customer1.should eql customer2 + # double-check that the stripe customer db record got it's email synced + customer = user.fetch_stripe_customer + customer.email.should eql 'unique+1@jamkazam.com' + + end + end + + describe "can_buy_test_drive?" do + let(:user) { FactoryGirl.create(:user) } + after { + Timecop.return + } + it "works" do + user.can_buy_test_drive?.should be true + FactoryGirl.create(:test_drive_purchase, user: user) + user.can_buy_test_drive?.should be false + Timecop.freeze(Date.today + 366) + user.can_buy_test_drive?.should be true + end + end + + describe "has_rated_teacher" do + + let(:user) { FactoryGirl.create(:user) } + let(:teacher) { FactoryGirl.create(:teacher) } + it "works" do + user.has_rated_teacher(teacher).should eql false + review = Review.create(target:teacher, rating:3, user: user) + review.errors[:target].should eql ["You must have at least scheduled or been in a lesson with this teacher"] + normal_lesson(user, teacher.user) + review = Review.create(target:teacher, rating:3, user: user) + review.errors.any?.should be false + user.has_rated_teacher(teacher).should be true + end + end + + describe "recent_test_drive_teachers" do + let(:user) { FactoryGirl.create(:user) } + let(:teacher) { FactoryGirl.create(:teacher_user) } + it "works" do + user.recent_test_drive_teachers.all.length.should eql 0 + + testdrive_lesson(user, teacher) + + user.recent_test_drive_teachers[0].id.should eql teacher.id + + end + end + + describe "update_name" do + + let(:user) {FactoryGirl.create(:user)} + + it "typical 2 bits" do + user.update_name("Seth Call") + user.errors.any?.should be_false + user.reload + user.first_name.should eql 'Seth' + user.last_name.should eql 'Call' + end + end + + describe "uncollectables" do + let(:user) {FactoryGirl.create(:user)} + let(:teacher) {FactoryGirl.create(:teacher_user)} + + + it "empty" do + user.uncollectables.count.should eql 0 + end + + it "one" do + lesson_session = normal_lesson(user, teacher) + lesson_session.lesson_payment_charge.user.should eql user + lesson_session.lesson_payment_charge.billing_attempts = 1 + lesson_session.lesson_payment_charge.save! + uncollectables = user.uncollectables + uncollectables.count.should eql 1 + uncollectable = uncollectables[0] + uncollectable.description.should_not be_nil + uncollectable.expected_price_in_cents.should eql 3000 + uncollectable.is_card_declined?.should be_false + end + + it "for monthly" do + Timecop.travel(Date.new(2016,1,1)) + lesson_session = monthly_lesson(user, teacher) + lesson_session.booked_price.should eql 30.00 + LessonBooking.hourly_check + lesson_session.lesson_payment_charge.should be_nil + purchases=LessonPackagePurchase.where(user_id: user.id) + purchases.count.should eql 1 + purchases[0].lesson_payment_charge.billed = false + purchases[0].lesson_payment_charge.billing_attempts = 1 + purchases[0].lesson_payment_charge.save! + uncollectables = user.uncollectables + uncollectables.count.should eql 1 + uncollectable = uncollectables[0] + uncollectable.description.should_not be_nil + uncollectable.expected_price_in_cents.should eql 3000 + uncollectable.is_card_declined?.should be_false + end + + it "for monthly near end of month" do + Timecop.travel(Date.new(2016,5,23)) + lesson_session = monthly_lesson(user, teacher) + lesson_session.booked_price.should eql 30.00 + LessonBooking.hourly_check + lesson_session.lesson_payment_charge.should be_nil + purchases=LessonPackagePurchase.where(user_id: user.id) + purchases.count.should eql 1 + purchases[0].lesson_payment_charge.billed = false + purchases[0].lesson_payment_charge.billing_attempts = 1 + purchases[0].lesson_payment_charge.save! + uncollectables = user.uncollectables + uncollectables.count.should eql 1 + uncollectable = uncollectables[0] + uncollectable.description.should_not be_nil + uncollectable.expected_price_in_cents.should eql 750 + uncollectable.is_card_declined?.should be_false + end + end + describe "handle_test_drive_package" do + let(:user) {FactoryGirl.create(:user)} + + it "4-count" do + package_size = 4 + package = FactoryGirl.create(:test_drive_package, :four_pack) + detail = {} + teachers = [] + detail[:teachers] = teachers + package.test_drive_package_teachers.each do |package_teacher| + teachers << {id: package_teacher.user.id} + end + user.handle_test_drive_package(package, detail) + + user.errors.any?.should be_false + + LessonSession.where(user_id: user.id).count.should eql package_size + user.student_lesson_bookings.count.should eql package_size + user.student_lesson_bookings.each do |booking| + booking.status.should eql LessonBooking::STATUS_REQUESTED + booking.card_presumed_ok.should be_false + end + end + end =begin describe "update avatar" do diff --git a/ruby/spec/jam_ruby/resque/google_analytics_event_spec.rb b/ruby/spec/jam_ruby/resque/google_analytics_event_spec.rb index 587080fe5..835916cc1 100644 --- a/ruby/spec/jam_ruby/resque/google_analytics_event_spec.rb +++ b/ruby/spec/jam_ruby/resque/google_analytics_event_spec.rb @@ -10,6 +10,7 @@ describe GoogleAnalyticsEvent do describe "track band analytics" do it 'reports first recording' do + pending "job is commented out" ResqueSpec.reset! user = FactoryGirl.create(:user) band = FactoryGirl.create(:band) @@ -26,6 +27,7 @@ describe GoogleAnalyticsEvent do end it 'reports first real session' do + pending "job is commented out" ResqueSpec.reset! JamRuby::GoogleAnalyticsEvent::BandSessionTracker.should have_schedule_size_of(0) user = FactoryGirl.create(:user) @@ -72,6 +74,7 @@ describe GoogleAnalyticsEvent do ResqueSpec.reset! end it 'reports size increment' do + pending "job is commented out" user = FactoryGirl.create(:user) music_session = FactoryGirl.create(:active_music_session, :creator => user, @@ -86,6 +89,7 @@ describe GoogleAnalyticsEvent do end it 'reports duration' do + pending "job is commented out" user = FactoryGirl.create(:user) JamRuby::GoogleAnalyticsEvent::SessionDurationTracker.should have_schedule_size_of(0) music_session = FactoryGirl.create(:active_music_session, diff --git a/ruby/spec/mailers/render_emails_spec.rb b/ruby/spec/mailers/render_emails_spec.rb index dbbe9648e..2f7cc727c 100644 --- a/ruby/spec/mailers/render_emails_spec.rb +++ b/ruby/spec/mailers/render_emails_spec.rb @@ -26,18 +26,187 @@ describe "RenderMailers", :slow => true do end it { @filename="welcome_message"; UserMailer.welcome_message(user).deliver_now } + it { @filename="student_welcome_message"; UserMailer.student_welcome_message(user).deliver_now } + it { @filename="school_owner_welcome_message"; UserMailer.school_owner_welcome_message(user).deliver_now } it { @filename="confirm_email"; UserMailer.confirm_email(user, "/signup").deliver_now } it { @filename="password_reset"; UserMailer.password_reset(user, '/reset_password').deliver_now } it { @filename="password_changed"; UserMailer.password_changed(user).deliver_now } it { @filename="updated_email"; UserMailer.updated_email(user).deliver_now } it { @filename="updating_email"; UserMailer.updating_email(user).deliver_now } + describe "has sending user" do let(:user2) { FactoryGirl.create(:user) } - let(:friend_request) {FactoryGirl.create(:friend_request, user:user, friend: user2)} + let(:friend_request) { FactoryGirl.create(:friend_request, user: user, friend: user2) } it { @filename="text_message"; UserMailer.text_message(user, user2.id, user2.name, user2.resolved_photo_url, 'Get online!!').deliver_now } - it { @filename="friend_request"; UserMailer.friend_request(user, 'So and so has sent you a friend request.', friend_request.id).deliver_now} + it { @filename="friend_request"; UserMailer.friend_request(user, 'So and so has sent you a friend request.', friend_request.id).deliver_now } + end + + describe "student/teacher" do + let(:teacher) { u = FactoryGirl.create(:teacher); u.user } + let(:user) { FactoryGirl.create(:user) } + + it "teacher_welcome_message" do + @filename = "teacher_welcome_message" + UserMailer.teacher_welcome_message(teacher).deliver_now + end + + it "teacher_counter_reminder" do + @filename = "teacher_counter_reminder" + + lesson_session = testdrive_lesson(user, teacher) + + UserMailer.deliveries.clear + UserMailer.teacher_counter_reminder(lesson_session).deliver_now + end + + it "teacher_lesson_request" do + @filename = "teacher_lesson_request" + + lesson_booking = testdrive_lesson(user, teacher).lesson_booking + + UserMailer.deliveries.clear + UserMailer.teacher_lesson_request(lesson_booking).deliver_now + end + + it "student_lesson_request" do + @filename = "student_lesson_request" + + lesson_booking = testdrive_lesson(user, teacher).lesson_booking + UserMailer.deliveries.clear + UserMailer.student_lesson_request(lesson_booking).deliver_now + end + + it "teacher_lesson_accepted" do + @filename = "teacher_lesson_accepted" + + lesson_session = testdrive_lesson(user, teacher) + UserMailer.deliveries.clear + UserMailer.teacher_lesson_accepted(lesson_session, "custom message", lesson_session.lesson_booking.default_slot).deliver_now + end + + it "student_lesson_accepted" do + @filename = "student_lesson_accepted" + + lesson_session = testdrive_lesson(user, teacher) + UserMailer.deliveries.clear + UserMailer.student_lesson_accepted(lesson_session, "custom message", lesson_session.lesson_booking.default_slot).deliver_now + end + + it "teacher_scheduled_jamclass_invitation" do + @filename = "teacher_scheduled_jamclass_invitation" + lesson_session = testdrive_lesson(user, teacher) + UserMailer.deliveries.clear + UserMailer.teacher_scheduled_jamclass_invitation(lesson_session.teacher, "custom message", lesson_session.music_session).deliver_now + end + + it "student_scheduled_jamclass_invitation" do + @filename = "student_scheduled_jamclass_invitation" + lesson_session = testdrive_lesson(user, teacher) + UserMailer.deliveries.clear + UserMailer.student_scheduled_jamclass_invitation(lesson_session.student, "custom message", lesson_session.music_session).deliver_now + end + + it "student_test_drive_no_bill" do + @filename = "student_test_drive_no_bill" + lesson = testdrive_lesson(user, teacher, {accept: true, miss: true}) + + UserMailer.deliveries.clear + UserMailer.student_test_drive_no_bill(lesson).deliver_now + end + + it "teacher_test_drive_no_bill" do + @filename = "teacher_test_drive_no_bill" + lesson = testdrive_lesson(user, teacher, {accept: true, miss: true}) + + UserMailer.deliveries.clear + UserMailer.teacher_test_drive_no_bill(lesson).deliver_now + end + it "student_test_drive_lesson_completed" do + @filename = "student_test_drive_lesson_completed" + lesson = testdrive_lesson(user, teacher) + + UserMailer.deliveries.clear + UserMailer.student_test_drive_lesson_completed(lesson).deliver_now + end + it "student_test_drive_done" do + @filename = "student_test_drive_lesson_done" + lesson = testdrive_lesson(user, teacher) + lesson = testdrive_lesson(user, FactoryGirl.create(:teacher_user)) + lesson = testdrive_lesson(user, FactoryGirl.create(:teacher_user)) + lesson = testdrive_lesson(user, FactoryGirl.create(:teacher_user)) + + UserMailer.deliveries.clear + UserMailer.student_test_drive_lesson_done(lesson).deliver_now + end + it "teacher_lesson_completed" do + @filename = "teacher_lesson_completed" + lesson = testdrive_lesson(user, teacher) + + UserMailer.deliveries.clear + UserMailer.teacher_lesson_completed(lesson).deliver_now + end + + it "lesson_starting_soon_teacher" do + @filename = "lesson_starting_soon_teacher" + lesson = testdrive_lesson(user, teacher) + + UserMailer.deliveries.clear + UserMailer.lesson_starting_soon_teacher(lesson).deliver_now + end + + it "lesson_starting_soon_student" do + @filename = "lesson_starting_soon_student" + lesson = testdrive_lesson(user, teacher) + + UserMailer.deliveries.clear + UserMailer.lesson_starting_soon_student(lesson).deliver_now + end + + it "music_notation_attachment" do + @filename = "music_notation_attachment" + + lesson = testdrive_lesson(user, teacher) + + UserMailer.deliveries.clear + notation = FactoryGirl.create(:music_notation, user: user) + UserMailer.lesson_attachment(user, teacher, lesson, notation).deliver_now + end + + it "recording_attachment" do + @filename = "recording_attachment" + + lesson = testdrive_lesson(user, teacher) + + UserMailer.deliveries.clear + claim = FactoryGirl.create(:claimed_recording, user: user) + UserMailer.lesson_attachment(user, teacher, lesson, claim).deliver_now + end + end + end + + describe "InvitedSchool emails" do + before(:each) do + UserMailer.deliveries.clear + end + + after(:each) do + UserMailer.deliveries.length.should == 1 + mail = UserMailer.deliveries[0] + save_emails_to_disk(mail, @filename) + end + + + + it "invite_school_teacher" do + @filename = "invite_school_teacher" + UserMailer.invite_school_teacher(FactoryGirl.create(:school_invitation, as_teacher: true)).deliver_now + end + + it "invite_school_student" do + @filename = "invite_school_student" + UserMailer.invite_school_student(FactoryGirl.create(:school_invitation, as_teacher: false)).deliver_now end end @@ -120,11 +289,13 @@ describe "RenderMailers", :slow => true do after(:each) do BatchMailer.deliveries.length.should == 1 - mail = BatchMailer.deliveries[0] + mail = BatchMailer.deliveries[0] save_emails_to_disk(mail, @filename) end - it "daily sessions" do @filename="daily_sessions"; scheduled_batch.deliver_batch end + it "daily sessions" do + @filename="daily_sessions"; scheduled_batch.deliver_batch + end end end @@ -140,7 +311,7 @@ def save_emails_to_disk(mail, filename) email_output_dir = 'tmp/emails' FileUtils.mkdir_p(email_output_dir) unless File.directory?(email_output_dir) filename = "#{filename}.eml" - File.open(File.join(email_output_dir, filename), "w+") {|f| + File.open(File.join(email_output_dir, filename), "w+") { |f| f << mail.encoded } end \ No newline at end of file diff --git a/ruby/spec/mailers/teacher_lesson_email_spec.rb b/ruby/spec/mailers/teacher_lesson_email_spec.rb new file mode 100644 index 000000000..9373b3eaa --- /dev/null +++ b/ruby/spec/mailers/teacher_lesson_email_spec.rb @@ -0,0 +1,366 @@ +# verifies that teacher directed emails go to the right location +require "spec_helper" + + +# https://jamkazam.atlassian.net/browse/VRFS-4128#add-comment +# * *teacher-always* : the teacher always gets this email +# * *school-over-teacher*: if there is a school, then the school gets the email if it has the scheduling communication pref set to true; otherwise, just teacher +# * *school-and-teacher*: if there is a school, send to both school and teacher; otherwise, just teacher + + +describe "TeacherLessonEmails" do + + def school_over_teacher + if @noSchool + mail.to.should eql [teacher.email] + elsif @schoolDirectComm + mail.to.should eql [teacher.email] + else + mail.to.should eql [school.communication_email] + end + end + + def teacher_always + mail.to.should eql [teacher.email] + end + + def school_and_teacher + if @noSchool + mail.to.should eql [teacher.email] + elsif @schoolDirectComm + mail.to.should eql [teacher.email] + else + mail.to.should eql [school.communication_email, teacher.email] + end + end + + before(:each) { + UserMailer.deliveries.clear + @noSchool = @schooDirectComm = @schoolSchedulingComm = false + } + # lessons with no school affiliation + describe "no-school" do + + let(:user) { FactoryGirl.create(:user) } + let(:teacher) { FactoryGirl.create(:teacher_user) } + let(:lesson) { normal_lesson(user, teacher) } + let(:mail) { UserMailer.deliveries[-1] } + + before(:each) { + @noSchool = true + } + + it "teacher_lesson_request" do + UserMailer.teacher_lesson_request(lesson.lesson_booking).deliver_now + school_over_teacher + end + + it "teacher_lesson_accepted" do + UserMailer.teacher_lesson_accepted(lesson, "come along now and teach", lesson.lesson_booking.lesson_booking_slots[0]).deliver_now + school_and_teacher + end + + it "teacher_lesson_update_all" do + UserMailer.teacher_lesson_update_all(lesson, "come along now and teach", lesson.lesson_booking.lesson_booking_slots[0]).deliver_now + school_and_teacher + end + + it "teacher_lesson_counter" do + UserMailer.teacher_lesson_counter(lesson, lesson.lesson_booking.lesson_booking_slots[0]).deliver_now + school_over_teacher + end + + it "teacher_lesson_normal_done" do + UserMailer.teacher_lesson_normal_done(lesson).deliver_now + school_over_teacher + end + + it "teacher_lesson_completed" do + UserMailer.teacher_lesson_completed(lesson).deliver_now + school_and_teacher + end + + it "teacher_test_drive_no_bill" do + UserMailer.teacher_test_drive_no_bill(lesson).deliver_now + school_and_teacher + end + + it "teacher_lesson_normal_no_bill" do + UserMailer.teacher_lesson_normal_no_bill(lesson).deliver_now + school_and_teacher + end + + it "teacher_unable_charge_monthly" do + pending "insufficient test setup" + paid = monthly_lesson(user, teacher, {accept:true, finish: true}) + paid.reload + UserMailer.teacher_unable_charge_monthly(paid.lesson_package_purchase).deliver_now + school_over_teacher + end + + it "teacher_lesson_monthly_charged" do + pending "insufficient test setup" + paid = monthly_lesson(user, teacher, {accept:true, finish: true}) + paid.reload + UserMailer.teacher_lesson_monthly_charged(paid.lesson_package_purchase).deliver_now + school_over_teacher + end + + it "teacher_distribution_done" do + teacher_payment = FactoryGirl.create(:teacher_payment, teacher: teacher, teacher_distribution: FactoryGirl.create(:teacher_distribution, lesson_session: lesson)) + UserMailer.teacher_distribution_done(teacher_payment).deliver_now + school_over_teacher + end + + it "teacher_distribution_fail" do + teacher_payment = FactoryGirl.create(:teacher_payment, teacher: teacher, teacher_distribution: FactoryGirl.create(:teacher_distribution, lesson_session: lesson)) + UserMailer.teacher_distribution_fail(teacher_payment).deliver_now + school_over_teacher + end + + it "teacher_lesson_booking_canceled" do + canceled_lesson = normal_lesson(user, teacher, {cancel: true}) + UserMailer.teacher_lesson_booking_canceled(canceled_lesson.lesson_booking, "Sorry I can't teach anymore!").deliver_now + school_and_teacher + end + + it "teacher_lesson_canceled" do + canceled_lesson = normal_lesson(user, teacher, {cancel: true}) + UserMailer.teacher_lesson_canceled(canceled_lesson, "Sorry I can't teach anymore!").deliver_now + school_and_teacher + end + + it "lesson_chat" do + msg = ChatMessage.create(user, nil, "nathuntoehun ", "lesson", "abc", teacher, lesson) + UserMailer.lesson_chat(msg).deliver_now + end + + it "teacher_counter_reminder" do + UserMailer.teacher_counter_reminder(lesson).deliver_now + school_over_teacher + end + + it "lesson_starting_soon_teacher" do + UserMailer.lesson_starting_soon_teacher(lesson).deliver_now + teacher_always + end + end + + # lessons with school affiliation, but teacher gets all communication + describe "school-but-direct-comm" do + before(:each) { + @schoolDirectComm = true + } + let(:school) { FactoryGirl.create(:school, scheduling_communication: School::SCHEDULING_COMM_TEACHER) } + let(:user) { FactoryGirl.create(:user) } + let(:teacher_obj) { FactoryGirl.create(:teacher, school: school) } + let(:teacher) { FactoryGirl.create(:user, teacher: teacher_obj) } + let(:lesson) { normal_lesson(user, teacher) } + let(:mail) { UserMailer.deliveries[-1] } + + + it "teacher_lesson_request" do + UserMailer.teacher_lesson_request(lesson.lesson_booking).deliver_now + school_over_teacher + end + + it "teacher_lesson_accepted" do + UserMailer.teacher_lesson_accepted(lesson, "come along now and teach", lesson.lesson_booking.lesson_booking_slots[0]).deliver_now + school_and_teacher + end + + it "teacher_lesson_update_all" do + UserMailer.teacher_lesson_update_all(lesson, "come along now and teach", lesson.lesson_booking.lesson_booking_slots[0]).deliver_now + school_and_teacher + end + + it "teacher_lesson_counter" do + UserMailer.teacher_lesson_counter(lesson, lesson.lesson_booking.lesson_booking_slots[0]).deliver_now + school_over_teacher + end + + it "teacher_lesson_normal_done" do + UserMailer.teacher_lesson_normal_done(lesson).deliver_now + school_over_teacher + end + + it "teacher_lesson_completed" do + UserMailer.teacher_lesson_completed(lesson).deliver_now + school_and_teacher + end + + it "teacher_test_drive_no_bill" do + UserMailer.teacher_test_drive_no_bill(lesson).deliver_now + school_and_teacher + end + + it "teacher_lesson_normal_no_bill" do + UserMailer.teacher_lesson_normal_no_bill(lesson).deliver_now + school_and_teacher + end + + it "teacher_unable_charge_monthly" do + pending "insufficient test setup" + paid = monthly_lesson(user, teacher, {accept:true, finish: true}) + paid.reload + UserMailer.teacher_unable_charge_monthly(paid.lesson_package_purchase).deliver_now + school_over_teacher + end + + it "teacher_lesson_monthly_charged" do + pending "insufficient test setup" + paid = monthly_lesson(user, teacher, {accept:true, finish: true}) + paid.reload + UserMailer.teacher_lesson_monthly_charged(paid.lesson_package_purchase).deliver_now + school_over_teacher + end + + it "teacher_distribution_done" do + teacher_payment = FactoryGirl.create(:teacher_payment, teacher: teacher, teacher_distribution: FactoryGirl.create(:teacher_distribution, lesson_session: lesson)) + UserMailer.teacher_distribution_done(teacher_payment).deliver_now + school_over_teacher + end + + it "teacher_distribution_fail" do + teacher_payment = FactoryGirl.create(:teacher_payment, teacher: teacher, teacher_distribution: FactoryGirl.create(:teacher_distribution, lesson_session: lesson)) + UserMailer.teacher_distribution_fail(teacher_payment).deliver_now + school_over_teacher + end + + it "teacher_lesson_booking_canceled" do + canceled_lesson = normal_lesson(user, teacher, {cancel: true}) + UserMailer.teacher_lesson_booking_canceled(canceled_lesson.lesson_booking, "Sorry I can't teach anymore!").deliver_now + school_and_teacher + end + + it "teacher_lesson_canceled" do + canceled_lesson = normal_lesson(user, teacher, {cancel: true}) + UserMailer.teacher_lesson_canceled(canceled_lesson, "Sorry I can't teach anymore!").deliver_now + school_and_teacher + end + + it "lesson_chat" do + msg = ChatMessage.create(user, nil, "nathuntoehun ", "lesson", "abc", teacher, lesson) + UserMailer.lesson_chat(msg).deliver_now + end + + it "teacher_counter_reminder" do + UserMailer.teacher_counter_reminder(lesson).deliver_now + school_over_teacher + end + + it "lesson_starting_soon_teacher" do + UserMailer.lesson_starting_soon_teacher(lesson).deliver_now + teacher_always + end + + end + + # lessons with school affiliation, and they want communication + describe "school-with-scheduling-comm" do + before(:each) { + @schoolSchedulingComm = true + } + let(:school) { FactoryGirl.create(:school, scheduling_communication: School::SCHEDULING_COMM_SCHOOL) } + let(:user) { FactoryGirl.create(:user) } + let(:teacher_obj) { FactoryGirl.create(:teacher, school: school) } + let(:teacher) { FactoryGirl.create(:user, teacher: teacher_obj) } + let(:lesson) { normal_lesson(user, teacher) } + let(:mail) { UserMailer.deliveries[-1] } + + it "teacher_lesson_request" do + UserMailer.teacher_lesson_request(lesson.lesson_booking).deliver_now + school_over_teacher + end + + it "teacher_lesson_accepted" do + UserMailer.teacher_lesson_accepted(lesson, "come along now and teach", lesson.lesson_booking.lesson_booking_slots[0]).deliver_now + school_and_teacher + end + + it "teacher_lesson_update_all" do + UserMailer.teacher_lesson_update_all(lesson, "come along now and teach", lesson.lesson_booking.lesson_booking_slots[0]).deliver_now + school_and_teacher + end + + it "teacher_lesson_counter" do + UserMailer.teacher_lesson_counter(lesson, lesson.lesson_booking.lesson_booking_slots[0]).deliver_now + school_over_teacher + end + + it "teacher_lesson_normal_done" do + UserMailer.teacher_lesson_normal_done(lesson).deliver_now + school_over_teacher + end + + it "teacher_lesson_completed" do + UserMailer.teacher_lesson_completed(lesson).deliver_now + school_and_teacher + end + + it "teacher_test_drive_no_bill" do + UserMailer.teacher_test_drive_no_bill(lesson).deliver_now + school_and_teacher + end + + it "teacher_lesson_normal_no_bill" do + UserMailer.teacher_lesson_normal_no_bill(lesson).deliver_now + school_and_teacher + end + + it "teacher_unable_charge_monthly" do + pending "insufficient test setup" + paid = monthly_lesson(user, teacher, {accept:true, finish: true}) + paid.reload + UserMailer.teacher_unable_charge_monthly(paid.lesson_package_purchase).deliver_now + school_over_teacher + end + + it "teacher_lesson_monthly_charged" do + pending "insufficient test setup" + paid = monthly_lesson(user, teacher, {accept:true, finish: true}) + paid.reload + UserMailer.teacher_lesson_monthly_charged(paid.lesson_package_purchase).deliver_now + school_over_teacher + end + + it "teacher_distribution_done" do + teacher_payment = FactoryGirl.create(:teacher_payment, teacher: teacher, teacher_distribution: FactoryGirl.create(:teacher_distribution, lesson_session: lesson)) + UserMailer.teacher_distribution_done(teacher_payment).deliver_now + school_over_teacher + end + + it "teacher_distribution_fail" do + teacher_payment = FactoryGirl.create(:teacher_payment, teacher: teacher, teacher_distribution: FactoryGirl.create(:teacher_distribution, lesson_session: lesson)) + UserMailer.teacher_distribution_fail(teacher_payment).deliver_now + school_over_teacher + end + + it "teacher_lesson_booking_canceled" do + canceled_lesson = normal_lesson(user, teacher, {cancel: true}) + UserMailer.teacher_lesson_booking_canceled(canceled_lesson.lesson_booking, "Sorry I can't teach anymore!").deliver_now + school_and_teacher + end + + it "teacher_lesson_canceled" do + canceled_lesson = normal_lesson(user, teacher, {cancel: true}) + UserMailer.teacher_lesson_canceled(canceled_lesson, "Sorry I can't teach anymore!").deliver_now + school_and_teacher + end + + it "lesson_chat" do + msg = ChatMessage.create(user, nil, "nathuntoehun ", "lesson", "abc", teacher, lesson) + UserMailer.lesson_chat(msg).deliver_now + end + + it "teacher_counter_reminder" do + UserMailer.teacher_counter_reminder(lesson).deliver_now + school_over_teacher + end + + it "lesson_starting_soon_teacher" do + UserMailer.lesson_starting_soon_teacher(lesson).deliver_now + teacher_always + end + end +end diff --git a/ruby/spec/mailers/user_mailer_spec.rb b/ruby/spec/mailers/user_mailer_spec.rb index ca64fa0a7..ec7db7133 100644 --- a/ruby/spec/mailers/user_mailer_spec.rb +++ b/ruby/spec/mailers/user_mailer_spec.rb @@ -185,7 +185,7 @@ describe UserMailer do # let(:mail) { UserMailer.deliveries[0] } # before(:each) do - # UserMailer.new_musicians(user, User.musicians).deliver + # UserMailer.new_musicians(user, User.musicians).deliver_now # end # it { UserMailer.deliveries.length.should == 1 } diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb index 5ddb4db2a..f4ecf15ff 100644 --- a/ruby/spec/spec_helper.rb +++ b/ruby/spec/spec_helper.rb @@ -5,12 +5,14 @@ require 'simplecov' require 'support/utilities' require 'support/profile' require 'support/maxmind' +require 'support/lesson_session' require 'active_record' require 'jam_db' require 'spec_db' require 'uses_temp_files' require 'resque_spec' require 'resque_failed_job_mailer' +require 'stripe_mock' # to prevent embedded resque code from forking ENV['FORK_PER_JOB'] = 'false' @@ -60,6 +62,8 @@ CarrierWave.configure do |config| config.enable_processing = false end +Stripe.api_key = "sk_test_OkjoIF7FmdjunyNsdVqJD02D" + #uncomment the following line to use spork with the debugger #require 'spork/ext/ruby-debug' @@ -95,13 +99,13 @@ end config.before(:suite) do DatabaseCleaner.strategy = :transaction - DatabaseCleaner.clean_with(:deletion, {pre_count: true, reset_ids:false, :except => %w[instruments genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] }) + DatabaseCleaner.clean_with(:deletion, {pre_count: true, reset_ids:false, :except => %w[gift_card_types lesson_package_types instruments languages subjects genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] }) end config.around(:each) do |example| # set no_transaction: true as metadata on your test to use deletion strategy instead if example.metadata[:no_transaction] - DatabaseCleaner.strategy = :deletion, {pre_count: true, reset_ids:false, :except => %w[instruments genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] } + DatabaseCleaner.strategy = :deletion, {pre_count: true, reset_ids:false, :except => %w[gift_card_types lesson_package_types instruments languages subjects genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] } else DatabaseCleaner.strategy = :transaction end @@ -115,6 +119,7 @@ end end config.after(:each) do + Timecop.return DatabaseCleaner.clean end @@ -161,7 +166,6 @@ end end #end - #Spork.each_run do # This code will be run each time you run your specs. #end diff --git a/ruby/spec/support/lesson_session.rb b/ruby/spec/support/lesson_session.rb new file mode 100644 index 000000000..badc7c8d8 --- /dev/null +++ b/ruby/spec/support/lesson_session.rb @@ -0,0 +1,160 @@ +module StripeMock + class ErrorQueue + def clear + @queue = [] + end + end + + def self.clear_errors + instance.error_queue.clear if instance + client.error_queue.clear if client + end +end + +def book_lesson(user, teacher, options) + + if options[:package_count].nil? + options[:package_count] = 4 + end + + if options[:slots].nil? + slots = [] + if options[:monthly] + slots << FactoryGirl.build(:lesson_booking_slot_recurring) + slots << FactoryGirl.build(:lesson_booking_slot_recurring) + else + slots << FactoryGirl.build(:lesson_booking_slot_single) + slots << FactoryGirl.build(:lesson_booking_slot_single) + end + + else + slots = options[:slots] + end + + if user.stored_credit_card == false + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', normal: true}) + #user.stored_credit_card = true + #user.save! + end + + if options[:test_drive] + booking = LessonBooking.book_test_drive(user, teacher, slots, "Hey I've heard of you before.") + elsif options[:normal] + booking = LessonBooking.book_normal(user, teacher, slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + elsif options[:monthly] + booking = LessonBooking.book_normal(user, teacher, slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + end + if booking.errors.any? + puts "BOOKING #{booking.errors.inspect}" + + if booking.lesson_booking_slots[0].errors.any? + puts "SLOT0 #{booking.lesson_booking_slots[0].errors.inspect}" + end + end + + booking.errors.any?.should be_false + lesson = booking.lesson_sessions[0] + start = lesson.scheduled_start + end_time = lesson.scheduled_start + (60 * lesson.duration) + + booking.card_presumed_ok.should be_true + + if options[:test_drive] + if user.most_recent_test_drive_purchase.nil? + LessonPackagePurchase.create(user, booking, LessonPackageType.package_for_test_drive_count(options[:package_count])) + end + elsif options[:monthly] + LessonPackagePurchase.create(user, booking, LessonPackageType.single, Date.today.year, Date.today.month) + end + + + if options[:counter] + if options[:monthly] + counter_slot = FactoryGirl.build(:lesson_booking_slot_recurring) + else + counter_slot = FactoryGirl.build(:lesson_booking_slot_single) + end + + lesson.counter({proposer: options[:counterer] || user, message: "countered YEAH!", slot: counter_slot}) + lesson.reload + lesson.status.should eql LessonSession::STATUS_COUNTERED + end + + if options[:accept] + lesson.accept({message: 'Yeah I got this', slot: slots[0].id, accepter: teacher}) + lesson.errors.any?.should be_false + lesson.reload + lesson.slot.should eql slots[0] + lesson.status.should eql LessonSession::STATUS_APPROVED + end + + if options[:cancel] + lesson.cancel({canceler: options[:canceler] || user, message: "sorry about that"}) + lesson.reload + lesson.status.should eql LessonSession::STATUS_CANCELED + end + + if options[:miss] + # teacher & student get into session + + Timecop.travel(end_time + 1) + lesson.analyse + lesson.session_completed + elsif options[:teacher_miss] + uh2 = FactoryGirl.create(:music_session_user_history, user: user, history: lesson.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson.music_session.session_removed_at = end_time + lesson.music_session.save! + Timecop.travel(end_time + 1) + lesson.analyse + lesson.session_completed + elsif options[:success] + uh1 = FactoryGirl.create(:music_session_user_history, user: user, history: lesson.music_session, created_at: start, session_removed_at: end_time) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher, history: lesson.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson.music_session.session_removed_at = end_time + lesson.music_session.save! + Timecop.travel(end_time + 1) + lesson.analyse + lesson.session_completed + elsif options[:finish] + # teacher & student get into session + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher, history: lesson.music_session, created_at: start, session_removed_at: end_time) + if options[:student_show] + uh2 = FactoryGirl.create(:music_session_user_history, user: user, history: lesson.music_session, created_at: start, session_removed_at: end_time) + end + # artificially end the session, which is covered by other background jobs + lesson.music_session.session_removed_at = end_time + lesson.music_session.save! + + Timecop.travel(end_time + 1) + + + lesson.analyse + lesson.session_completed + + if options[:monthly] + LessonBooking.hourly_check + end + end + + lesson +end + +def testdrive_lesson(user, teacher, options = {finish: false, accept: true, cancel: false, miss: false, slots: nil}) + options[:test_drive] = true + book_lesson(user, teacher, options) +end + + +def normal_lesson(user, teacher, options = {finish: false, accept: true, cancel: false, miss: false, slots: nil}) + options[:normal] = true + book_lesson(user, teacher, options) +end + + +def monthly_lesson(user, teacher, options = {finish: false, accept: true, cancel: false, miss: false, slots: nil}) + options[:monthly] = true + book_lesson(user, teacher, options) +end \ No newline at end of file diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 2717f63dc..ca203612f 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -7,10 +7,22 @@ def app_config 'http://localhost:3333' end + def email_partners_alias + 'partners@jamkazam.com' + end + + def email_social_alias + 'social@jamkazam.com' + end + def email_alerts_alias 'alerts@jamkazam.com' end + def email_crashes_alias + 'clientcrash@jamkazam.com' + end + def email_recurly_notice 'recurly-alerts@jamkazam.com' end @@ -47,6 +59,10 @@ def app_config '315576000' end + def verify_email_enabled + false + end + def audiomixer_path # you can specify full path to audiomixer with AUDIOMIXER_PATH env variable... # or we check for audiomixer path in the user's workspace @@ -179,7 +195,7 @@ def app_config end def signing_job_queue_max_time - 20 # 20 seconds + 600 # 20 seconds end def one_free_jamtrack_per_user @@ -210,6 +226,88 @@ def app_config "AIzaSyCPTPq5PEcl4XWcm7NZ2IGClZlbsiE8JNo" end + def estimated_jam_track_time + 40 + end + + def estimated_fast_mixdown_time + 30 + end + + def estimated_slow_mixdown_time + 80 + end + + def num_packaging_nodes + 2 + end + + def signing_job_signing_max_time + 300 + end + + def mixdown_job_queue_max_time + 600 + end + + def mixdown_step_max_time + 300 + end + + def download_tracker_day_range + 30 + end + + def guard_against_browser_fraud + true + end + + def minimum_lesson_booking_hrs + 24 + end + + def lesson_stay_time + 10 + end + + def wait_time_window_threshold + 5 + end + + def lesson_together_threshold_minutes + 5 + end + def lesson_join_time_window_minutes + 5 + end + def lesson_wait_time_window_minutes + 10 + end + def wait_time_window_pct + 0.8 + end + def end_of_wait_window_forgiveness_minutes + 1 + end + + def test_drive_wait_period_year + 1 + end + + def check_bounced_emails + false + end + def stripe + { + :publishable_key => 'pk_test_HLTvioRAxN3hr5fNfrztZeoX', + :secret_key => 'sk_test_OkjoIF7FmdjunyNsdVqJD02D', + :source_customer => 'cus_88Vp44SLnBWMXq', # seth@jamkazam.com in JamKazam-test account + :ach_pct => 0.008 + } + end + def musician_count + '40,000+' + end private def audiomixer_workspace_path @@ -222,7 +320,6 @@ def app_config dev_path = "#{dev_path}/audiomixer/audiomixer/audiomixerapp" dev_path if File.exist? dev_path end - end klass.new @@ -280,3 +377,54 @@ def friend(user1, user2) FactoryGirl.create(:friendship, user: user1, friend: user2) FactoryGirl.create(:friendship, user: user2, friend: user1) end + +def stripe_oauth_client + # from here in the JamKazam Test account: https://dashboard.stripe.com/account/applications/settings + client_id = "ca_88T6HlHg1NRyKFossgWIz1Tf431Jshft" + options = { + :site => 'https://connect.stripe.com', + :authorize_url => '/oauth/authorize', + :token_url => '/oauth/token' + } + + @stripe_oauth_client ||= OAuth2::Client.new(client_id, APP_CONFIG.stripe[:publishable_key], options) +end + +def stripe_account2_id + # seth+stripe+test1@jamkazam.com / jam12345 + "acct_17sCNpDcwjPgpqRL" +end + def stripe_account1_id + # seth+stripe1@jamkazam.com / jam123 +=begin + curl -X POST https://connect.stripe.com/oauth/token \ +-d client_secret=sk_test_OkjoIF7FmdjunyNsdVqJD02D \ +-d code=ac_88U1TDwBgao4I3uYyyFEO3pVbbEed6tm \ +-d grant_type=authorization_code + +# { + "access_token": "sk_test_q8WZbdQXt7RGRkBR0fhgohG6", + "livemode": false, + "refresh_token": "rt_88U3csV42HtY1P1Cd9KU2GCez3wixgsHtIHaQbeeu1dXVWo9", + "token_type": "bearer", + "stripe_publishable_key": "pk_test_s1YZDczylyRUvhAGeVhxqznp", + "stripe_user_id": "acct_17sCNpDcwjPgpqRL", + "scope": "read_write" +} +=end + + + "acct_17sCEyH8FcKpNSnR" + +end + +def create_stripe_token(exp_month = 2017) + Stripe::Token.create( + :card => { + :number => "4111111111111111", + :exp_month => 2, + :exp_year => exp_month, + :cvc => "314" + }, + ).id +end diff --git a/web/Gemfile b/web/Gemfile index e7755e4c5..f2ffac5b3 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -27,6 +27,8 @@ gem 'sprockets-es6', require: 'sprockets/es6' gem 'sprockets-rails', '2.3.2' #gem 'license_finder' +gem 'pg_migrate', '0.1.14' +gem 'kickbox' gem 'oj', '2.10.2' gem 'builder' gem 'jquery-rails' @@ -48,12 +50,18 @@ gem 'eventmachine', '1.0.4' gem 'faraday', '~>0.9.0' gem 'amqp', '0.9.8' #gem 'logging-rails', :require => 'logging/rails' -gem 'omniauth', '1.1.1' -gem 'omniauth-facebook', '1.4.1' +#gem 'omniauth', '1.1.1' +#gem 'omniauth-facebook', '1.4.1' +#======= +#gem 'logging-rails', :require => 'logging/rails' +gem 'omniauth' +gem 'omniauth-facebook' +#>>>>>>> develop gem 'omniauth-twitter' -gem 'omniauth-google-oauth2', '0.2.1' -gem 'google-api-client', '0.7.1' -gem 'google-api-omniauth', '0.1.1' +gem 'omniauth-google-oauth2' +gem 'omniauth-stripe-connect' +gem 'google-api-client' #, '0.7.1' +#gem 'google-api-omniauth' #, '0.1.1' gem 'signet', '0.5.0' gem 'twitter' gem 'fb_graph', '2.5.9' @@ -65,11 +73,12 @@ gem 'carmen' gem 'carrierwave' #, '0.9.0' gem 'carrierwave_direct' gem 'fog' -gem 'jquery-payment-rails', github: 'sethcall/jquery-payment-rails' +#gem 'jquery-payment-rails', github: 'sethcall/jquery-payment-rails' gem 'haml-rails' gem 'unf' #optional fog dependency gem 'devise', '3.3.0' #3.4.0 causes uninitialized constant ActionController::Metal (NameError) gem 'postgres-copy' +gem 'prawn-table' #group :libv8 do # gem 'libv8', "~> 3.11.8" #end @@ -94,19 +103,35 @@ gem 'htmlentities' gem 'sanitize' gem 'recurly' #gem 'guard', '2.7.3' -gem 'influxdb', '0.1.8' -gem 'influxdb-rails', '0.1.10' +#gem 'influxdb' #, '0.1.8' +gem 'cause' # needed by influxdb +gem 'influxdb-rails'# , '0.1.10' gem 'sitemap_generator' gem 'bower-rails', "~> 0.9.2" gem 'react-rails', '~> 1.0' +gem 'sendgrid_toolkit', '>= 1.1.1' +gem 'stripe' +gem 'zip-codes' +gem 'email_validator' #gem "browserify-rails", "~> 0.7" + +if ENV['FASTER_PATH'] == '1' + # https://github.com/danielpclark/faster_path + # supposed to dramatically speed up page load time. Gotta install rust. go to github if interested + gem 'faster_path', '~> 0.1.0', :group => :development +end + source 'https://rails-assets.org' do - gem 'rails-assets-reflux' + gem 'rails-assets-reflux', '0.3.0' gem 'rails-assets-classnames' - gem 'rails-assets-react-select' +#<<<<<<< HEAD +# gem 'rails-assets-react-select' #gem "rails-assets-regenerator" gem 'rails-assets-bluebird' +#======= + gem 'rails-assets-react-select', '0.6.7' +#>>>>>>> develop end #group :development, :production do @@ -126,7 +151,7 @@ group :development, :test do gem 'test-unit' # gem 'teaspoon' # gem 'teaspoon-jasmine' - gem 'puma' +# gem 'puma' gem 'byebug' end group :unix do @@ -143,6 +168,7 @@ group :test, :cucumber do gem 'simplecov', '~> 0.7.1' gem 'simplecov-rcov' gem 'capybara' # '2.4.4' + gem 'rails-assets-sinon', source: 'https://rails-assets.org' #if ENV['JAMWEB_QT5'] == '1' # # necessary on platforms such as arch linux, where pacman -S qt5-webkit is your easiet option # gem "capybara-webkit", :git => 'git://github.com/thoughtbot/capybara-webkit.git' @@ -160,6 +186,7 @@ group :test, :cucumber do # gem 'growl', '1.0.3' gem 'poltergeist' gem 'resque_spec' + gem 'timecop' #gem 'thin' end diff --git a/web/README.md b/web/README.md index 2b5096424..730c0f2f5 100644 --- a/web/README.md +++ b/web/README.md @@ -2,6 +2,4 @@ Jasmine Javascript Unit Tests ============================= -Open browser to localhost:3000/teaspoon - - +Open browser to localhost:3000/teaspoon \ No newline at end of file diff --git a/web/Rakefile b/web/Rakefile index 882005f42..609810566 100644 --- a/web/Rakefile +++ b/web/Rakefile @@ -8,5 +8,6 @@ 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/fontawesome/fontawesome-webfont.eot b/web/app/assets/fonts/fontawesome/fontawesome-webfont.eot new file mode 100644 index 000000000..33b2bb800 Binary files /dev/null and b/web/app/assets/fonts/fontawesome/fontawesome-webfont.eot differ diff --git a/web/app/assets/fonts/fontawesome/fontawesome-webfont.svg b/web/app/assets/fonts/fontawesome/fontawesome-webfont.svg new file mode 100644 index 000000000..1ee89d436 --- /dev/null +++ b/web/app/assets/fonts/fontawesome/fontawesome-webfont.svg @@ -0,0 +1,565 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/app/assets/fonts/fontawesome/fontawesome-webfont.ttf b/web/app/assets/fonts/fontawesome/fontawesome-webfont.ttf new file mode 100644 index 000000000..ed9372f8e Binary files /dev/null and b/web/app/assets/fonts/fontawesome/fontawesome-webfont.ttf differ diff --git a/web/app/assets/fonts/fontawesome/fontawesome-webfont.woff b/web/app/assets/fonts/fontawesome/fontawesome-webfont.woff new file mode 100644 index 000000000..8b280b98f Binary files /dev/null and b/web/app/assets/fonts/fontawesome/fontawesome-webfont.woff differ diff --git a/web/app/assets/fonts/fontawesome/fontawesome-webfont.woff2 b/web/app/assets/fonts/fontawesome/fontawesome-webfont.woff2 new file mode 100644 index 000000000..3311d5851 Binary files /dev/null and b/web/app/assets/fonts/fontawesome/fontawesome-webfont.woff2 differ diff --git a/web/app/assets/fonts/iconfontcustom/iconfontcustom.eot b/web/app/assets/fonts/iconfontcustom/iconfontcustom.eot new file mode 100644 index 000000000..ce2bbc08a Binary files /dev/null and b/web/app/assets/fonts/iconfontcustom/iconfontcustom.eot differ diff --git a/web/app/assets/fonts/iconfontcustom/iconfontcustom.svg b/web/app/assets/fonts/iconfontcustom/iconfontcustom.svg new file mode 100644 index 000000000..ce64a7dbd --- /dev/null +++ b/web/app/assets/fonts/iconfontcustom/iconfontcustom.svg @@ -0,0 +1,721 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/app/assets/fonts/iconfontcustom/iconfontcustom.ttf b/web/app/assets/fonts/iconfontcustom/iconfontcustom.ttf new file mode 100644 index 000000000..c3e74b7b3 Binary files /dev/null and b/web/app/assets/fonts/iconfontcustom/iconfontcustom.ttf differ diff --git a/web/app/assets/fonts/iconfontcustom/iconfontcustom.woff b/web/app/assets/fonts/iconfontcustom/iconfontcustom.woff new file mode 100644 index 000000000..af5e1ed7b Binary files /dev/null and b/web/app/assets/fonts/iconfontcustom/iconfontcustom.woff differ diff --git a/web/app/assets/images/content/badge-download-on-the-app-store.svg b/web/app/assets/images/content/badge-download-on-the-app-store.svg new file mode 100644 index 000000000..ac111e597 --- /dev/null +++ b/web/app/assets/images/content/badge-download-on-the-app-store.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/app/assets/images/content/bkg_home_jamclass.jpg b/web/app/assets/images/content/bkg_home_jamclass.jpg new file mode 100644 index 000000000..2d01fec74 Binary files /dev/null and b/web/app/assets/images/content/bkg_home_jamclass.jpg differ diff --git a/web/app/assets/images/content/bkg_home_jamclass_x.jpg b/web/app/assets/images/content/bkg_home_jamclass_x.jpg new file mode 100644 index 000000000..1c78c7233 Binary files /dev/null and b/web/app/assets/images/content/bkg_home_jamclass_x.jpg differ diff --git a/web/app/assets/images/content/icon-delete.png b/web/app/assets/images/content/icon-delete.png new file mode 100644 index 000000000..cae694fba Binary files /dev/null and b/web/app/assets/images/content/icon-delete.png differ diff --git a/web/app/assets/images/content/icon-delete@2X.png b/web/app/assets/images/content/icon-delete@2X.png new file mode 100644 index 000000000..b120befa5 Binary files /dev/null and b/web/app/assets/images/content/icon-delete@2X.png differ diff --git a/web/app/assets/images/content/icon-edit.png b/web/app/assets/images/content/icon-edit.png new file mode 100644 index 000000000..26ed5892f Binary files /dev/null and b/web/app/assets/images/content/icon-edit.png differ diff --git a/web/app/assets/images/content/icon-edit@2X.png b/web/app/assets/images/content/icon-edit@2X.png new file mode 100644 index 000000000..7cd9052d5 Binary files /dev/null and b/web/app/assets/images/content/icon-edit@2X.png differ diff --git a/web/app/assets/images/content/icon-mix-fail@2X.png b/web/app/assets/images/content/icon-mix-fail@2X.png new file mode 100644 index 000000000..265022204 Binary files /dev/null and b/web/app/assets/images/content/icon-mix-fail@2X.png differ diff --git a/web/app/assets/images/content/icon-play.png b/web/app/assets/images/content/icon-play.png new file mode 100644 index 000000000..ecb26e78c Binary files /dev/null and b/web/app/assets/images/content/icon-play.png differ diff --git a/web/app/assets/images/content/icon-retry@2X.png b/web/app/assets/images/content/icon-retry@2X.png new file mode 100644 index 000000000..584a0dca1 Binary files /dev/null and b/web/app/assets/images/content/icon-retry@2X.png differ diff --git a/web/app/assets/images/content/icon-save@2X.png b/web/app/assets/images/content/icon-save@2X.png new file mode 100644 index 000000000..ac80ef9c4 Binary files /dev/null and b/web/app/assets/images/content/icon-save@2X.png differ diff --git a/web/app/assets/images/content/icon_download.png b/web/app/assets/images/content/icon_download.png new file mode 100644 index 000000000..d56bde46c Binary files /dev/null and b/web/app/assets/images/content/icon_download.png differ diff --git a/web/app/assets/images/content/icon_download@2X.png b/web/app/assets/images/content/icon_download@2X.png new file mode 100644 index 000000000..5473ff7df Binary files /dev/null and b/web/app/assets/images/content/icon_download@2X.png differ diff --git a/web/app/assets/images/content/icon_download@3X.png b/web/app/assets/images/content/icon_download@3X.png new file mode 100644 index 000000000..13c2a1fd1 Binary files /dev/null and b/web/app/assets/images/content/icon_download@3X.png differ diff --git a/web/app/assets/images/content/icon_open@2X.png b/web/app/assets/images/content/icon_open@2X.png new file mode 100644 index 000000000..1d188a2ea Binary files /dev/null and b/web/app/assets/images/content/icon_open@2X.png differ diff --git a/web/app/assets/images/content/icon_unread_mail.png b/web/app/assets/images/content/icon_unread_mail.png new file mode 100644 index 000000000..32f098bf5 Binary files /dev/null and b/web/app/assets/images/content/icon_unread_mail.png differ diff --git a/web/app/assets/images/content/phone-icon.png b/web/app/assets/images/content/phone-icon.png new file mode 100644 index 000000000..fd52859fb Binary files /dev/null and b/web/app/assets/images/content/phone-icon.png differ diff --git a/web/app/assets/images/content/stripe-connect-blue-on-dark.png b/web/app/assets/images/content/stripe-connect-blue-on-dark.png new file mode 100644 index 000000000..aec105611 Binary files /dev/null and b/web/app/assets/images/content/stripe-connect-blue-on-dark.png differ diff --git a/web/app/assets/images/content/stripe-connect-light-on-dark.png b/web/app/assets/images/content/stripe-connect-light-on-dark.png new file mode 100644 index 000000000..f9d3433b3 Binary files /dev/null and b/web/app/assets/images/content/stripe-connect-light-on-dark.png differ diff --git a/web/app/assets/images/down_arrow_black_pad.png b/web/app/assets/images/down_arrow_black_pad.png new file mode 100644 index 000000000..38c86597a Binary files /dev/null and b/web/app/assets/images/down_arrow_black_pad.png differ diff --git a/web/app/assets/images/landing/Andy Crowley - Avatar.png b/web/app/assets/images/landing/Andy Crowley - Avatar.png new file mode 100644 index 000000000..4a675f607 Binary files /dev/null and b/web/app/assets/images/landing/Andy Crowley - Avatar.png differ diff --git a/web/app/assets/images/landing/Andy Crowley - Speech Bubble.png b/web/app/assets/images/landing/Andy Crowley - Speech Bubble.png new file mode 100644 index 000000000..9dc63b79b Binary files /dev/null and b/web/app/assets/images/landing/Andy Crowley - Speech Bubble.png differ diff --git a/web/app/assets/images/landing/Andy Crowley - YouTube.png b/web/app/assets/images/landing/Andy Crowley - YouTube.png new file mode 100644 index 000000000..0dad41bf8 Binary files /dev/null and b/web/app/assets/images/landing/Andy Crowley - YouTube.png differ diff --git a/web/app/assets/images/landing/Carl Brown - Avatar.png b/web/app/assets/images/landing/Carl Brown - Avatar.png new file mode 100644 index 000000000..a991220a3 Binary files /dev/null and b/web/app/assets/images/landing/Carl Brown - Avatar.png differ diff --git a/web/app/assets/images/landing/Carl Brown - Jam Class - Speech Bubble.png b/web/app/assets/images/landing/Carl Brown - Jam Class - Speech Bubble.png new file mode 100644 index 000000000..272faa6f1 Binary files /dev/null and b/web/app/assets/images/landing/Carl Brown - Jam Class - Speech Bubble.png differ diff --git a/web/app/assets/images/landing/Carl Brown - Speech Bubble.png b/web/app/assets/images/landing/Carl Brown - Speech Bubble.png new file mode 100644 index 000000000..071f8c40f Binary files /dev/null and b/web/app/assets/images/landing/Carl Brown - Speech Bubble.png differ diff --git a/web/app/assets/images/landing/Carl Brown - YouTube.png b/web/app/assets/images/landing/Carl Brown - YouTube.png new file mode 100644 index 000000000..289b170d3 Binary files /dev/null and b/web/app/assets/images/landing/Carl Brown - YouTube.png differ diff --git a/web/app/assets/images/landing/Dave Sebree - Avatar.png b/web/app/assets/images/landing/Dave Sebree - Avatar.png new file mode 100644 index 000000000..3de11a1c2 Binary files /dev/null and b/web/app/assets/images/landing/Dave Sebree - Avatar.png differ diff --git a/web/app/assets/images/landing/Dave Sebree - Jam Class - Speech Bubble.png b/web/app/assets/images/landing/Dave Sebree - Jam Class - Speech Bubble.png new file mode 100644 index 000000000..baa1db709 Binary files /dev/null and b/web/app/assets/images/landing/Dave Sebree - Jam Class - Speech Bubble.png differ diff --git a/web/app/assets/images/landing/JK_FBAd_Bass_with_Keys.png b/web/app/assets/images/landing/JK_FBAd_Bass_with_Keys.png new file mode 100644 index 000000000..27886fc13 Binary files /dev/null and b/web/app/assets/images/landing/JK_FBAd_Bass_with_Keys.png differ diff --git a/web/app/assets/images/landing/JK_FBAd_Guitar_with_Keys.png b/web/app/assets/images/landing/JK_FBAd_Guitar_with_Keys.png new file mode 100644 index 000000000..61d12e9aa Binary files /dev/null and b/web/app/assets/images/landing/JK_FBAd_Guitar_with_Keys.png differ diff --git a/web/app/assets/images/landing/Julie Bonk - Avatar.png b/web/app/assets/images/landing/Julie Bonk - Avatar.png new file mode 100644 index 000000000..0b5019588 Binary files /dev/null and b/web/app/assets/images/landing/Julie Bonk - Avatar.png differ diff --git a/web/app/assets/images/landing/Julie Bonk - Jam Class - Speech Bubble.png b/web/app/assets/images/landing/Julie Bonk - Jam Class - Speech Bubble.png new file mode 100644 index 000000000..e430826db Binary files /dev/null and b/web/app/assets/images/landing/Julie Bonk - Jam Class - Speech Bubble.png differ diff --git a/web/app/assets/images/landing/Justin Pierce - Avatar.png b/web/app/assets/images/landing/Justin Pierce - Avatar.png new file mode 100644 index 000000000..2e3c0530b Binary files /dev/null and b/web/app/assets/images/landing/Justin Pierce - Avatar.png differ diff --git a/web/app/assets/images/landing/Justin Pierce - Jam Class - Speech Bubble.png b/web/app/assets/images/landing/Justin Pierce - Jam Class - Speech Bubble.png new file mode 100644 index 000000000..d9504877e Binary files /dev/null and b/web/app/assets/images/landing/Justin Pierce - Jam Class - Speech Bubble.png differ diff --git a/web/app/assets/images/landing/Ryan Jones - Avatar.png b/web/app/assets/images/landing/Ryan Jones - Avatar.png new file mode 100644 index 000000000..09b908884 Binary files /dev/null and b/web/app/assets/images/landing/Ryan Jones - Avatar.png differ diff --git a/web/app/assets/images/landing/Ryan Jones - PianoKeyz - YouTube.png b/web/app/assets/images/landing/Ryan Jones - PianoKeyz - YouTube.png new file mode 100644 index 000000000..5c40b4727 Binary files /dev/null and b/web/app/assets/images/landing/Ryan Jones - PianoKeyz - YouTube.png differ diff --git a/web/app/assets/images/landing/Ryan Jones - Speech Bubble.png b/web/app/assets/images/landing/Ryan Jones - Speech Bubble.png new file mode 100644 index 000000000..9cf7c90d9 Binary files /dev/null and b/web/app/assets/images/landing/Ryan Jones - Speech Bubble.png differ diff --git a/web/app/assets/images/landing/Sara Nelson - Avatar.png b/web/app/assets/images/landing/Sara Nelson - Avatar.png new file mode 100644 index 000000000..3e899565c Binary files /dev/null and b/web/app/assets/images/landing/Sara Nelson - Avatar.png differ diff --git a/web/app/assets/images/landing/Sara Nelson - Jam Class - Speech Bubble.png b/web/app/assets/images/landing/Sara Nelson - Jam Class - Speech Bubble.png new file mode 100644 index 000000000..d803a7626 Binary files /dev/null and b/web/app/assets/images/landing/Sara Nelson - Jam Class - Speech Bubble.png differ diff --git a/web/app/assets/images/landing/Top 10 Image - Number 1.png b/web/app/assets/images/landing/Top 10 Image - Number 1.png new file mode 100644 index 000000000..82e1a3847 Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 1.png differ diff --git a/web/app/assets/images/landing/Top 10 Image - Number 10.png b/web/app/assets/images/landing/Top 10 Image - Number 10.png new file mode 100644 index 000000000..23f1fd862 Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 10.png differ diff --git a/web/app/assets/images/landing/Top 10 Image - Number 2.png b/web/app/assets/images/landing/Top 10 Image - Number 2.png new file mode 100644 index 000000000..9d79989d7 Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 2.png differ diff --git a/web/app/assets/images/landing/Top 10 Image - Number 3.png b/web/app/assets/images/landing/Top 10 Image - Number 3.png new file mode 100644 index 000000000..7084feaf4 Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 3.png differ diff --git a/web/app/assets/images/landing/Top 10 Image - Number 4.png b/web/app/assets/images/landing/Top 10 Image - Number 4.png new file mode 100644 index 000000000..48f40e9be Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 4.png differ diff --git a/web/app/assets/images/landing/Top 10 Image - Number 5.png b/web/app/assets/images/landing/Top 10 Image - Number 5.png new file mode 100644 index 000000000..d50812947 Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 5.png differ diff --git a/web/app/assets/images/landing/Top 10 Image - Number 6.png b/web/app/assets/images/landing/Top 10 Image - Number 6.png new file mode 100644 index 000000000..b76dd8555 Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 6.png differ diff --git a/web/app/assets/images/landing/Top 10 Image - Number 7.png b/web/app/assets/images/landing/Top 10 Image - Number 7.png new file mode 100644 index 000000000..0de272f34 Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 7.png differ diff --git a/web/app/assets/images/landing/Top 10 Image - Number 9.png b/web/app/assets/images/landing/Top 10 Image - Number 9.png new file mode 100644 index 000000000..8fdcb8d46 Binary files /dev/null and b/web/app/assets/images/landing/Top 10 Image - Number 9.png differ diff --git a/web/app/assets/images/landing/YouTube Logo.png b/web/app/assets/images/landing/YouTube Logo.png new file mode 100644 index 000000000..d58848cfc Binary files /dev/null and b/web/app/assets/images/landing/YouTube Logo.png differ diff --git a/web/app/assets/images/landing/ad-session-window.png b/web/app/assets/images/landing/ad-session-window.png new file mode 100644 index 000000000..a5bbd38c0 Binary files /dev/null and b/web/app/assets/images/landing/ad-session-window.png differ diff --git a/web/app/assets/images/landing/ad-video-window.png b/web/app/assets/images/landing/ad-video-window.png new file mode 100644 index 000000000..336772c07 Binary files /dev/null and b/web/app/assets/images/landing/ad-video-window.png differ diff --git a/web/app/assets/images/landing/arrow-1-student.png b/web/app/assets/images/landing/arrow-1-student.png new file mode 100644 index 000000000..14afba89c Binary files /dev/null and b/web/app/assets/images/landing/arrow-1-student.png differ diff --git a/web/app/assets/images/landing/arrow-jamblaster-order.png b/web/app/assets/images/landing/arrow-jamblaster-order.png new file mode 100644 index 000000000..50e0495ec Binary files /dev/null and b/web/app/assets/images/landing/arrow-jamblaster-order.png differ diff --git a/web/app/assets/images/landing/broadcast_video.png b/web/app/assets/images/landing/broadcast_video.png new file mode 100644 index 000000000..b3a281b60 Binary files /dev/null and b/web/app/assets/images/landing/broadcast_video.png differ diff --git a/web/app/assets/images/landing/find_musicians.png b/web/app/assets/images/landing/find_musicians.png new file mode 100644 index 000000000..b2160ba9d Binary files /dev/null and b/web/app/assets/images/landing/find_musicians.png differ diff --git a/web/app/assets/images/landing/gift_card.png b/web/app/assets/images/landing/gift_card.png new file mode 100644 index 000000000..e652e81db Binary files /dev/null and b/web/app/assets/images/landing/gift_card.png differ diff --git a/web/app/assets/images/landing/jam_class.png b/web/app/assets/images/landing/jam_class.png new file mode 100644 index 000000000..326816b97 Binary files /dev/null and b/web/app/assets/images/landing/jam_class.png differ diff --git a/web/app/assets/images/landing/jamtrack_landing_arrow_1.png b/web/app/assets/images/landing/jamtrack_landing_arrow_1.png new file mode 100644 index 000000000..c19bc6faf Binary files /dev/null and b/web/app/assets/images/landing/jamtrack_landing_arrow_1.png differ diff --git a/web/app/assets/images/landing/jamtrack_landing_arrow_2.png b/web/app/assets/images/landing/jamtrack_landing_arrow_2.png new file mode 100644 index 000000000..0cb8cb55a Binary files /dev/null and b/web/app/assets/images/landing/jamtrack_landing_arrow_2.png differ diff --git a/web/app/assets/images/landing/logo-composite.jpg b/web/app/assets/images/landing/logo-composite.jpg new file mode 100644 index 000000000..ab66248d0 Binary files /dev/null and b/web/app/assets/images/landing/logo-composite.jpg differ diff --git a/web/app/assets/images/landing/logo-composite.png b/web/app/assets/images/landing/logo-composite.png new file mode 100644 index 000000000..f78866d96 Binary files /dev/null and b/web/app/assets/images/landing/logo-composite.png differ diff --git a/web/app/assets/images/landing/marketplace.png b/web/app/assets/images/landing/marketplace.png new file mode 100644 index 000000000..f851fb77a Binary files /dev/null and b/web/app/assets/images/landing/marketplace.png differ diff --git a/web/app/assets/images/landing/online_lessons.png b/web/app/assets/images/landing/online_lessons.png new file mode 100644 index 000000000..7e4da04ec Binary files /dev/null and b/web/app/assets/images/landing/online_lessons.png differ diff --git a/web/app/assets/images/landing/phone_control_jamblaster.png b/web/app/assets/images/landing/phone_control_jamblaster.png new file mode 100644 index 000000000..4b28201e1 Binary files /dev/null and b/web/app/assets/images/landing/phone_control_jamblaster.png differ diff --git a/web/app/assets/images/landing/preorder_jamblaster.png b/web/app/assets/images/landing/preorder_jamblaster.png new file mode 100644 index 000000000..932f2089e Binary files /dev/null and b/web/app/assets/images/landing/preorder_jamblaster.png differ diff --git a/web/app/assets/images/landing/recording_with_video.png b/web/app/assets/images/landing/recording_with_video.png new file mode 100644 index 000000000..b07fbe180 Binary files /dev/null and b/web/app/assets/images/landing/recording_with_video.png differ diff --git a/web/app/assets/images/landing/student_landing_cta_arrow.png b/web/app/assets/images/landing/student_landing_cta_arrow.png new file mode 100644 index 000000000..6d0df7084 Binary files /dev/null and b/web/app/assets/images/landing/student_landing_cta_arrow.png differ diff --git a/web/app/assets/images/landing/teacher-icon-small.png b/web/app/assets/images/landing/teacher-icon-small.png new file mode 100644 index 000000000..cca2496ce Binary files /dev/null and b/web/app/assets/images/landing/teacher-icon-small.png differ diff --git a/web/app/assets/images/landing/teacher-icon.png b/web/app/assets/images/landing/teacher-icon.png new file mode 100644 index 000000000..175968b9b Binary files /dev/null and b/web/app/assets/images/landing/teacher-icon.png differ diff --git a/web/app/assets/images/landing/us_latency_diagram.png b/web/app/assets/images/landing/us_latency_diagram.png new file mode 100644 index 000000000..6dddd6256 Binary files /dev/null and b/web/app/assets/images/landing/us_latency_diagram.png differ diff --git a/web/app/assets/images/modern/jamblaster.png b/web/app/assets/images/modern/jamblaster.png new file mode 100644 index 000000000..e2f80f37a Binary files /dev/null and b/web/app/assets/images/modern/jamblaster.png differ diff --git a/web/app/assets/images/modern/jamclass.png b/web/app/assets/images/modern/jamclass.png new file mode 100644 index 000000000..4052d8d83 Binary files /dev/null and b/web/app/assets/images/modern/jamclass.png differ diff --git a/web/app/assets/images/modern/jamtracks.png b/web/app/assets/images/modern/jamtracks.png new file mode 100644 index 000000000..7bb9f3aef Binary files /dev/null and b/web/app/assets/images/modern/jamtracks.png differ diff --git a/web/app/assets/images/modern/jk-image.png b/web/app/assets/images/modern/jk-image.png new file mode 100644 index 000000000..79a7089a1 Binary files /dev/null and b/web/app/assets/images/modern/jk-image.png differ diff --git a/web/app/assets/images/modern/logo-2.png b/web/app/assets/images/modern/logo-2.png new file mode 100644 index 000000000..d7482a85d Binary files /dev/null and b/web/app/assets/images/modern/logo-2.png differ diff --git a/web/app/assets/images/modern/logo.png b/web/app/assets/images/modern/logo.png new file mode 100644 index 000000000..d7482a85d Binary files /dev/null and b/web/app/assets/images/modern/logo.png differ diff --git a/web/app/assets/images/modern/main-bkg.jpg b/web/app/assets/images/modern/main-bkg.jpg new file mode 100644 index 000000000..4ef28eb41 Binary files /dev/null and b/web/app/assets/images/modern/main-bkg.jpg differ diff --git a/web/app/assets/images/shared/mobile-preview-load.gif b/web/app/assets/images/shared/mobile-preview-load.gif new file mode 100755 index 000000000..d0dee5844 Binary files /dev/null and b/web/app/assets/images/shared/mobile-preview-load.gif differ diff --git a/web/app/assets/images/web/button_cta_jamblaster.png b/web/app/assets/images/web/button_cta_jamblaster.png index 8650fe322..932f2089e 100644 Binary files a/web/app/assets/images/web/button_cta_jamblaster.png 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 index 998d89b75..d8dd6e40c 100644 Binary files a/web/app/assets/images/web/button_cta_jamtrack.png 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 index dba8d499b..a2ddf74ef 100644 Binary files a/web/app/assets/images/web/button_cta_platform.png and b/web/app/assets/images/web/button_cta_platform.png differ diff --git a/web/app/assets/javascripts/AAB_message_factory.js b/web/app/assets/javascripts/AAB_message_factory.js index 2a502f43e..f75d62be4 100644 --- a/web/app/assets/javascripts/AAB_message_factory.js +++ b/web/app/assets/javascripts/AAB_message_factory.js @@ -20,6 +20,7 @@ UNSUBSCRIBE : "UNSUBSCRIBE", SUBSCRIPTION_MESSAGE : "SUBSCRIPTION_MESSAGE", SUBSCRIBE_BULK : "SUBSCRIBE_BULK", + USER_STATUS : "USER_STATUS", // friend notifications FRIEND_UPDATE : "FRIEND_UPDATE", @@ -50,6 +51,9 @@ SCHEDULED_SESSION_REMINDER : "SCHEDULED_SESSION_REMINDER", SCHEDULED_SESSION_COMMENT : "SCHEDULED_SESSION_COMMENT", + SCHEDULED_JAMCLASS_INVITATION : "SCHEDULED_JAMCLASS_INVITATION", + LESSON_MESSAGE : "LESSON_MESSAGE", + // recording notifications MUSICIAN_RECORDING_SAVED : "MUSICIAN_RECORDING_SAVED", BAND_RECORDING_SAVED : "BAND_RECORDING_SAVED", @@ -66,6 +70,7 @@ // text message TEXT_MESSAGE : "TEXT_MESSAGE", CHAT_MESSAGE : "CHAT_MESSAGE", + SEND_CHAT_MESSAGE : "SEND_CHAT_MESSAGE", // broadcast notifications SOURCE_UP_REQUESTED : "SOURCE_UP_REQUESTED", @@ -120,13 +125,30 @@ }; // Heartbeat message - factory.heartbeat = function(lastNotificationSeen, lastNotificationSeenAt) { + factory.heartbeat = function(lastNotificationSeen, lastNotificationSeenAt, active) { var data = {}; data.notification_seen = lastNotificationSeen; data.notification_seen_at = lastNotificationSeenAt; + data.active = active; return client_container(msg.HEARTBEAT, route_to.SERVER, data); }; + // User Status update message + factory.userStatus = function(active, status) { + var data = {}; + data.active = active; + data.status = status; + return client_container(msg.USER_STATUS, route_to.SERVER, data); + }; + + factory.chatMessage = function(channel, msg) { + var data = {} + data.channel = {} + data.msg = {} + + return client_container(msg.SEND_CHAT_MESSAGE, route_to.SERVER, data) + } + // create a login message using user/pass factory.login_with_user_pass = function(username, password) { var login = { username : username , password : password }; @@ -146,6 +168,11 @@ return client_container(msg.LOGIN, route_to.SERVER, login); }; + factory.logout = function() { + var logout = {} + return client_container(msg.LOGOUT, route_to.SERVER, logout); + } + // create a login message using only the client_id. only valid for latency_tester factory.login_with_client_id = function(client_id, client_type) { if(client_type != 'latency_tester') { diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index 01f36623b..8a1b045b3 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -23,6 +23,7 @@ var mode = null; // heartbeat + var startHeartbeatTimeout = null; var heartbeatInterval = null; var heartbeatMS = null; var connection_expire_time = null; @@ -35,6 +36,7 @@ var notificationLastSeen = undefined; var clientClosedConnection = false; var initialConnectAttempt = true; + var active = true; // reconnection logic var connectDeferred = null; @@ -45,6 +47,7 @@ var reconnectingWaitPeriodStart = null; var reconnectDueTime = null; var connectTimeout = null; + var activityTimeout; // elements var $inSituBanner = null; @@ -104,6 +107,12 @@ heartbeatInterval = null; } + // stop the heartbeat start delay from happening + if (startHeartbeatTimeout != null) { + clearTimeout(startHeartbeatTimeout); + startHeartbeatTimeout = null; + } + // stop checking for heartbeat acks if (heartbeatAckCheckInterval != null) { clearTimeout(heartbeatAckCheckInterval); @@ -166,7 +175,8 @@ function _heartbeat() { if (app.heartbeatActive) { - var message = context.JK.MessageFactory.heartbeat(notificationLastSeen, notificationLastSeenAt); + //console.log("heartbeat active?: " + active) + var message = context.JK.MessageFactory.heartbeat(notificationLastSeen, notificationLastSeenAt, active); notificationLastSeenAt = undefined; notificationLastSeen = undefined; // for debugging purposes, see if the last time we've sent a heartbeat is way off (500ms) of the target interval @@ -218,9 +228,9 @@ app.clientId = payload.client_id; - if (isClientMode()) { + if (isClientMode() && context.jamClient) { // tell the backend that we have logged in - context.jamClient.OnLoggedIn(payload.user_id, payload.token); // ACTS AS CONTINUATION + context.jamClient.OnLoggedIn(payload.user_id, payload.token, payload.username); // ACTS AS CONTINUATION $.cookie('client_id', payload.client_id); } @@ -236,9 +246,41 @@ heartbeatMS = payload.heartbeat_interval * 1000; connection_expire_time = payload.connection_expire_time * 1000; logger.info("loggedIn(): clientId=" + app.clientId + " heartbeat=" + payload.heartbeat_interval + "s expire_time=" + payload.connection_expire_time + 's'); - heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS); - heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000); - lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat + + // add some randomness to help move heartbeats apart from each other + + // send 1st heartbeat somewhere between 0 - 0.5 of the connection expire time + var randomStartTime = connection_expire_time * (Math.random() / 2) + + if (startHeartbeatTimeout) { + logger.warn("start heartbeat timeout is active; should be null") + clearTimeout(startHeartbeatTimeout) + } + + if (heartbeatInterval != null) { + logger.warn("heartbeatInterval is active; should be null") + clearInterval(heartbeatInterval); + heartbeatInterval = null; + } + + if (heartbeatAckCheckInterval != null) { + logger.warn("heartbeatAckCheckInterval is active; should be null") + clearInterval(heartbeatAckCheckInterval); + heartbeatAckCheckInterval = null; + } + + startHeartbeatTimeout = setTimeout(function() { + if(server.connected) { + heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS); + heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000); + lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat + } + }, randomStartTime) + + logger.info("starting heartbeat timer in " + randomStartTime/1000 + 's') + + + connectDeferred.resolve(); $self.triggerHandler(EVENTS.CONNECTION_UP) @@ -250,6 +292,40 @@ }, 0) } + function markAway() { + logger.debug("sleep again!") + active = false; + context.UserActivityActions.setActive(active) + var userStatus = msg_factory.userStatus(false, null); + server.send(userStatus); + } + + function activityCheck() { + var timeoutTime = 300000; // 5 * 1000 * 60 , 5 minutes + active = true; + context.UserActivityActions.setActive(active) + activityTimeout = setTimeout(markAway, timeoutTime); + $(document).ready(function() { + $('body').bind('mousedown keydown touchstart focus', function(event) { + if (activityTimeout) { + clearTimeout(activityTimeout); + activityTimeout = null; + } + + if (!active) { + if(server && server.connected) { + logger.debug("awake again!") + var userStatus = msg_factory.userStatus(true, null); + server.send(userStatus); + } + } + active = true; + context.UserActivityActions.setActive(active) + activityTimeout = setTimeout(markAway, timeoutTime); + }); + }); + } + function heartbeatAck(header, payload) { lastHeartbeatAckTime = new Date(); } @@ -281,7 +357,7 @@ function socketClosed(in_error) { // tell the backend that we have logged out - context.jamClient.OnLoggedOut(); + if (context.jamClient) context.jamClient.OnLoggedOut(); } @@ -295,6 +371,11 @@ logger.debug(payload.error_code + ": no longer reconnecting") server.noReconnect = true; // stop trying to log in!! } + else if (payload.error_code == 'no_reconnect') { + logger.debug(payload.error_code + ": no longer reconnecting") + server.noReconnect = true; // stop trying to log in!! + context.JK.Banner.showAlert("Misbehaved Client", "Please restart your application in order to continue using JamKazam.") + } } /////////////////// @@ -384,7 +465,7 @@ } function formatDelaySecs(secs) { - return $('' + secs + ' ' + (secs == 1 ? ' second.s' : 'seconds.') + ''); + return $('' + secs + ' ' + (secs == 1 ? ' second.s' : 'seconds.') + ''); } function setCountdown($parent) { @@ -548,7 +629,10 @@ clientType = context.JK.clientType(); } if(!mode) { - mode = context.jamClient.getOperatingMode ? context.jamClient.getOperatingMode() : 'client'; + mode = 'client' + if (context.jamClient && context.jamClient.getOperatingMode) { + mode = context.jamClient.getOperatingMode() + } } connectDeferred = new $.Deferred(); @@ -573,6 +657,7 @@ client_type: isClientMode() ? context.JK.clientType() : 'latency_tester', client_id: isClientMode() ? (gon.global.env == "development" ? $.cookie('client_id') : null): context.jamClient.clientID, os: context.JK.GetOSAsString(), + jamblaster_serial_no: context.PlatformStore.jamblasterSerialNo(), udp_reachable: context.JK.StunInstance ? !context.JK.StunInstance.sync() : null // latency tester doesn't have the stun class loaded } @@ -723,6 +808,31 @@ //console.timeEnd('sendP2PMessage'); }; + server.sendLogin = function (token) { + logger.debug("sending login message on behalf of client") + var outgoing_msg = msg_factory.login_with_token(token, null, null); + server.send(outgoing_msg); + }; + + server.sendLogout = function () { + logger.debug("sending logout message on behalf of client") + var outgoing_msg = msg_factory.logout(); + server.send(outgoing_msg); + }; + + server.sendChatMessage = function(channel, message) { + + if (server.connected) { + + var chatMsg = msg_factory.chatMessage(channel, message) + server.send(chatMsg) + return true; + } + else { + return false; + } + } + server.updateNotificationSeen = function (notificationId, notificationCreatedAt) { var time = new Date(notificationCreatedAt); @@ -760,6 +870,11 @@ // Callbacks from jamClient if (context.jamClient !== undefined && context.jamClient.IsNativeClient()) { context.jamClient.SendP2PMessage.connect(server.sendP2PMessage); + + if (context.jamClient.SendLogin) { + context.jamClient.SendLogin.connect(server.sendLogin); + context.jamClient.SendLogin.connect(server.sendLogout); + } } function initialize() { @@ -768,6 +883,7 @@ registerHeartbeatAck(); registerServerRejection(); registerSocketClosed(); + activityCheck(); $inSituBanner = $('.server-connection'); $inSituBannerHolder = $('.no-websocket-connection'); diff --git a/web/app/assets/javascripts/accounts.js b/web/app/assets/javascripts/accounts.js index c04a6b329..6f144f06a 100644 --- a/web/app/assets/javascripts/accounts.js +++ b/web/app/assets/javascripts/accounts.js @@ -47,7 +47,11 @@ if(gon.global.video_available && gon.global.video_available!="none" ) { var webcamName; - var webcam = context.jamClient.FTUECurrentSelectedVideoDevice() + var webcam = null; + if (context.jamClient.FTUECurrentSelectedVideoDevice) { + webcam = context.jamClient.FTUECurrentSelectedVideoDevice() + } + if (webcam == null || typeof(webcam) == "undefined" || Object.keys(webcam).length == 0) { webcamName = "None Configured" } else { @@ -75,6 +79,7 @@ is_affiliate_partner: userDetail.is_affiliate_partner, affiliate_earnings: (userDetail.affiliate_earnings / 100).toFixed(2), affiliate_referral_count: userDetail.affiliate_referral_count, + owns_school: !!userDetail.owned_school_id, webcamName: webcamName } , { variable: 'data' })); @@ -139,6 +144,7 @@ $("#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; } ); + $("#account-content-scroller").on('click', '#account-school-link', function(evt) {evt.stopPropagation(); navToSchool(); return false; } ); } function renderAccount() { @@ -166,11 +172,11 @@ function navToEditProfile() { resetForm() - window.location = '/client#/account/profile' + window.ProfileActions.startProfileEdit(null, false) } function navToEditSubscriptions() { - window.location = '/client#/account/profile' + window.ProfileActions.startProfileEdit(null, false) } function navToEditPayments() { @@ -196,6 +202,11 @@ window.location = '/client#/account/affiliatePartner' } + function navToSchool() { + resetForm() + window.location = '/client#/account/school' + } + // handle update avatar event function updateAvatar(avatar_url) { var photoUrl = context.JK.resolveAvatarUrl(avatar_url); diff --git a/web/app/assets/javascripts/accounts_jamtracks.js.coffee b/web/app/assets/javascripts/accounts_jamtracks.js.coffee index 08785bc50..5793d7941 100644 --- a/web/app/assets/javascripts/accounts_jamtracks.js.coffee +++ b/web/app/assets/javascripts/accounts_jamtracks.js.coffee @@ -19,7 +19,7 @@ context.JK.AccountJamTracks = class AccountJamTracks @screen = $('#account-jamtracks') beforeShow:() => - rest.getPurchasedJamTracks({}) + rest.getPurchasedJamTracks({limit: 40}) .done(@populateJamTracks) .fail(@app.ajaxError); diff --git a/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee b/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee index 82dbe73eb..a605afd01 100644 --- a/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee +++ b/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee @@ -40,8 +40,6 @@ context.JK.AccountPaymentHistoryScreen = class AccountPaymentHistoryScreen @noMoreSales.hide() @next = null - - refresh:() => @currentQuery = this.buildQuery() @rest.getSalesHistory(@currentQuery) diff --git a/web/app/assets/javascripts/accounts_profile.js b/web/app/assets/javascripts/accounts_profile.js index 9938d4cf0..d2c325343 100644 --- a/web/app/assets/javascripts/accounts_profile.js +++ b/web/app/assets/javascripts/accounts_profile.js @@ -1,12 +1,13 @@ -(function(context,$) { +(function (context, $) { "use strict"; context.JK = context.JK || {}; - context.JK.AccountProfileScreen = function(app) { + context.JK.AccountProfileScreen = function (app) { var $document = $(document); var logger = context.JK.logger; var EVENTS = context.JK.EVENTS; + var NAMED_MESSAGES = context.JK.NAMED_MESSAGES; var api = context.JK.Rest(); var userId; var user = {}; @@ -32,12 +33,18 @@ var $btnSubmit = $screen.find('.account-edit-profile-submit'); function beforeShow(data) { - userId = data.id; + userId = data.id; } function afterShow(data) { - resetForm(); - renderAccountProfile(); + if (window.ProfileStore.solo) { + $btnSubmit.text('SAVE & RETURN TO PROFILE'); + } + else { + $btnSubmit.text('SAVE & NEXT'); + } + resetForm(); + renderAccountProfile(); } function resetForm() { @@ -64,7 +71,7 @@ var content_root = $('#account-profile-content-scroller'); // set birth_date - if(userDetail.birth_date) { + if (userDetail.birth_date) { var birthDateFields = userDetail.birth_date.split('-') var birthDateYear = birthDateFields[0]; var birthDateMonth = birthDateFields[1]; @@ -74,13 +81,11 @@ $('select#user_birth_date_2i', content_root).val(parseInt(birthDateMonth)); $('select#user_birth_date_3i', content_root).val(parseInt(birthDateDay)); } - - context.JK.dropdown($('select', content_root)); } function populateAccountProfileLocation(userDetail, regions, cities) { - populateRegions(regions, userDetail.state); - populateCities(cities, userDetail.city); + populateRegions(regions, userDetail.state); + populateCities(cities, userDetail.city); } function populateCountries(countries, userCountry) { @@ -94,21 +99,21 @@ nilOption.text(nilOptionText); countrySelect.append(nilOption); - $.each(countries, function(index, country) { - if(!country) return; + $.each(countries, function (index, country) { + if (!country) return; var option = $(nilOptionStr); option.text(country); option.attr("value", country); - if(country == userCountry) { + if (country == userCountry) { foundCountry = true; } countrySelect.append(option); }); - if(!foundCountry) { + if (!foundCountry) { // in this case, the user has a country that is not in the database // this can happen in a development/test scenario, but let's assume it can // happen in production too. @@ -120,8 +125,6 @@ countrySelect.val(userCountry); countrySelect.attr("disabled", null) - - context.JK.dropdown(countrySelect); } @@ -137,21 +140,21 @@ nilOption.text(nilOptionText); countrySelect.append(nilOption); - $.each(countriesx, function(index, countryx) { + $.each(countriesx, function (index, countryx) { if (!countryx.countrycode) return; var option = $(nilOptionStr); option.text(countryx.countryname); option.attr("value", countryx.countrycode); - if(countryx.countrycode == userCountry) { + if (countryx.countrycode == userCountry) { foundCountry = true; } countrySelect.append(option); }); - if(!foundCountry) { + if (!foundCountry) { // in this case, the user has a country that is not in the database // this can happen in a development/test scenario, but let's assume it can // happen in production too. @@ -163,8 +166,6 @@ countrySelect.val(userCountry); countrySelect.attr("disabled", null); - - context.JK.dropdown(countrySelect); } function populateRegions(regions, userRegion) { @@ -175,8 +176,8 @@ nilOption.text(nilOptionText); regionSelect.append(nilOption); - $.each(regions, function(index, region) { - if(!region) return; + $.each(regions, function (index, region) { + if (!region) return; var option = $(nilOptionStr); option.text(region['name']); @@ -187,8 +188,6 @@ regionSelect.val(userRegion) regionSelect.attr("disabled", null) - - context.JK.dropdown(regionSelect); } function populateCities(cities, userCity) { @@ -199,8 +198,8 @@ nilOption.text(nilOptionText); citySelect.append(nilOption); - $.each(cities, function(index, city) { - if(!city) return; + $.each(cities, function (index, city) { + if (!city) return; var option = $(nilOptionStr); option.text(city); @@ -211,35 +210,46 @@ citySelect.val(userCity); citySelect.attr("disabled", null); - - context.JK.dropdown(citySelect); } /****************** MAIN PORTION OF SCREEN *****************/ // events for main screen function events() { - $btnCancel.click(function(evt) { + $btnCancel.click(function (evt) { evt.stopPropagation(); - navToAccount(); + window.ProfileActions.cancelProfileEdit() + return false; + }); + + $('#account-profile-content-scroller').on('click', '#account-change-avatar', function (evt) { + evt.stopPropagation(); + navToAvatar(); return false; }); - - $('#account-profile-content-scroller').on('click', '#account-change-avatar', function(evt) { evt.stopPropagation(); navToAvatar(); return false; } ); enableSubmits(); } + function teacherGuidance() { + + if(recentUserDetail && recentUserDetail.is_a_teacher) { + setTimeout(function() { + var $header = $('#account-edit-profile-form h2') + context.JK.HelpBubbleHelper.teacherMusicianProfile($header, $screen); + }, 2000) + } + + } function renderAccountProfile() { + $.when(api.getUserProfile()) - .done(function(userDetail) { + .done(function (userDetail) { recentUserDetail = userDetail; populateAccountProfile(userDetail); - + teacherGuidance(); selectLocation = new context.JK.SelectLocation(getCountryElement(), getRegionElement(), getCityElement(), app); selectLocation.load(userDetail.country, userDetail.state, userDetail.city) }); - - context.JK.dropdown($('select')); } function navToAccount() { @@ -277,20 +287,24 @@ biography: biography, subscribe_email: subscribeEmail }) - .done(postUpdateProfileSuccess) - .fail(postUpdateProfileFailure) - .always(enableSubmits) + .done(postUpdateProfileSuccess) + .fail(postUpdateProfileFailure) + .always(enableSubmits) } function enableSubmits() { - $btnSubmit.click(function(evt) { + $btnSubmit.click(function (evt) { evt.stopPropagation(); handleUpdateProfile(); return false; }); - $btnSubmit.removeClass("disabled"); - $('#account-profile-content-scroller').on('submit', '#account-edit-email-form', function(evt) { evt.stopPropagation(); handleUpdateProfile(); return false; } ); - $("#account-edit-email-form").removeClass("disabled"); + $btnSubmit.removeClass("disabled"); + $('#account-profile-content-scroller').on('submit', '#account-edit-email-form', function (evt) { + evt.stopPropagation(); + handleUpdateProfile(); + return false; + }); + $("#account-edit-email-form").removeClass("disabled"); } function disableSubmits() { @@ -302,14 +316,14 @@ function postUpdateProfileSuccess(response) { $document.triggerHandler(EVENTS.USER_UPDATED, response); - context.location = "/client#/account/profile/experience"; + window.ProfileActions.editProfileNext('experience'); } function postUpdateProfileFailure(xhr, textStatus, errorMessage) { var errors = JSON.parse(xhr.responseText) - if(xhr.status == 422) { + if (xhr.status == 422) { var first_name = context.JK.format_errors("first_name", errors); var last_name = context.JK.format_errors("last_name", errors); var country = context.JK.format_errors("country", errors); @@ -320,41 +334,45 @@ var subscribeEmail = context.JK.format_errors("subscribe_email", errors); var biography = context.JK.format_errors("biography", errors); - if(first_name != null) { + if (first_name != null) { getFirstNameElement().closest('div.field').addClass('error').end().after(first_name); } - if(last_name != null) { + if (last_name != null) { getLastNameElement().closest('div.field').addClass('error').end().after(last_name); } - if(country != null) { + if (country != null) { getCountryElement().closest('div.field').addClass('error').end().after(country); } - if(state != null) { + if (state != null) { getRegionElement().closest('div.field').addClass('error').end().after(state); } - if(city != null) { + if (city != null) { getCityElement().closest('div.field').addClass('error').end().after(city); } - if(birth_date != null) { + if (birth_date != null) { getYearElement().closest('div.field').addClass('error').end().after(birth_date); } - if(subscribeEmail != null) { + if (subscribeEmail != null) { getSubscribeEmail().closest('div.field').addClass('error').end().after(subscribeEmail); } - if(gender != null) { + if (gender != null) { getGenderElement().closest('div.field').addClass('error').end().after(gender); } + + if (biography != null) { + getBiographyElement().closest('div.field').addClass('error').end().after(biography); + } } else { app.ajaxError(xhr, textStatus, errorMessage) - } + } } function handleCountryChanged() { @@ -376,9 +394,9 @@ regionElement.children().remove() regionElement.append($(nilOptionStr).text('loading...')) - api.getRegions({ country: selectedCountry }) + api.getRegions({country: selectedCountry}) .done(getRegionsDone) - .error(function(err) { + .error(function (err) { regionElement.children().remove() regionElement.append($(nilOptionStr).text(nilOptionText)) }) @@ -404,20 +422,19 @@ cityElement.children().remove(); cityElement.append($(nilOptionStr).text('loading...')); - api.getCities({ country: selectedCountry, region: selectedRegion }) + api.getCities({country: selectedCountry, region: selectedRegion}) .done(getCitiesDone) - .error(function(err) { - cityElement.children().remove(); - cityElement.append($(nilOptionStr).text(nilOptionText)); + .error(function (err) { + cityElement.children().remove(); + cityElement.append($(nilOptionStr).text(nilOptionText)); }) .always(function () { - loadingCitiesData = false; + loadingCitiesData = false; }); } else { cityElement.children().remove(); cityElement.append($(nilOptionStr).text(nilOptionText)); - context.JK.dropdown(cityElement); } } @@ -478,12 +495,16 @@ return $('#account-profile-content-scroller input[name=subscribe_email]'); } + function getBiographyElement() { + return $('#account-profile-content-scroller textarea[name=biography]'); + } + function getBirthDate() { var month = getMonthElement().val() var day = getDayElement().val() var year = getYearElement().val() - if(month != null && month.length > 0 && day != null && day.length > 0 && year != null && year.length > 0) { + if (month != null && month.length > 0 && day != null && day.length > 0 && year != null && year.length > 0) { return month + "-" + day + "-" + year; } else { @@ -506,4 +527,4 @@ return this; }; -})(window,jQuery); \ No newline at end of file +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/accounts_profile_avatar.js b/web/app/assets/javascripts/accounts_profile_avatar.js index 63d1aca1f..de47e1228 100644 --- a/web/app/assets/javascripts/accounts_profile_avatar.js +++ b/web/app/assets/javascripts/accounts_profile_avatar.js @@ -40,8 +40,8 @@ var template= context.JK.fillTemplate($('#template-account-profile-avatar').html(), { "fp_apikey" : gon.fp_apikey, "data-fp-store-path" : createStorePath(userDetail) + createOriginalFilename(userDetail), - "fp_policy" : filepicker_policy.policy, - "fp_signature" : filepicker_policy.signature + "fp_policy" : encodeURIComponent(filepicker_policy.policy), + "fp_signature" : encodeURIComponent(filepicker_policy.signature) }); $('#account-profile-avatar-content-scroller').html(template); @@ -126,7 +126,7 @@ maxSize: 10000*1024, policy: filepickerPolicy.policy, signature: filepickerPolicy.signature - }, { path: createStorePath(self.userDetail), access: 'public' }, + }, { path: createStorePath(self.userDetail), access: 'public' }, function(fpfiles) { removeAvatarSpinner(); afterImageUpload(fpfiles[0]); @@ -202,7 +202,6 @@ renderNoAvatar(avatarSpace); } else { - rest.getFilepickerPolicy({handle: fpfile.url}) .done(function(filepickerPolicy) { avatarSpace.children().remove(); diff --git a/web/app/assets/javascripts/accounts_profile_experience.js b/web/app/assets/javascripts/accounts_profile_experience.js index de57e8695..c058edb1f 100644 --- a/web/app/assets/javascripts/accounts_profile_experience.js +++ b/web/app/assets/javascripts/accounts_profile_experience.js @@ -21,6 +21,16 @@ } function afterShow(data) { + + if (window.ProfileStore.solo) { + $btnBack.hide() + $btnSubmit.text('SAVE & RETURN TO PROFILE'); + } + else { + $btnBack.show() + $btnSubmit.text('SAVE & NEXT'); + } + resetForm(); renderExperience(); } @@ -54,8 +64,6 @@ $screen.find('select[name=skill_level]').val(userDetail.skill_level); $screen.find('select[name=concert_count]').val(userDetail.concert_count); $screen.find('select[name=studio_session_count]').val(userDetail.studio_session_count); - - context.JK.dropdown($('select', $screen)); } function isUserInstrument(instrument, userInstruments) { @@ -115,7 +123,7 @@ function events() { $btnCancel.click(function(evt) { evt.stopPropagation(); - navigateTo('/client#/profile/' + context.JK.currentUserId); + window.ProfileActions.cancelProfileEdit() return false; }); @@ -149,8 +157,6 @@ var userDetail = userDetailResponse[0]; populateAccountProfile(userDetail, instrumentsResponse[0]); }); - - context.JK.dropdown($('select')); } function navigateTo(targetLocation) { @@ -179,7 +185,7 @@ function postUpdateProfileSuccess(response) { $document.triggerHandler(EVENTS.USER_UPDATED, response); - context.location = "/client#/account/profile/interests"; + ProfileActions.editProfileNext('interests') } function postUpdateProfileFailure(xhr, textStatus, errorMessage) { diff --git a/web/app/assets/javascripts/accounts_profile_interests.js b/web/app/assets/javascripts/accounts_profile_interests.js index 84b205777..562b142dd 100644 --- a/web/app/assets/javascripts/accounts_profile_interests.js +++ b/web/app/assets/javascripts/accounts_profile_interests.js @@ -70,7 +70,16 @@ } function afterShow(data) { - renderInterests() + if (window.ProfileStore.solo) { + $btnBack.hide() + $btnSubmit.text('SAVE & RETURN TO PROFILE'); + } + else { + $btnBack.show() + $btnSubmit.text('SAVE & NEXT'); + } + + renderInterests() } function resetForm() { @@ -121,19 +130,24 @@ // Column 2 - genres var genres = profileUtils.virtualBandGenreList(userDetail.genres) - $virtualBandGenreList.html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) + var genreIds = profileUtils.getGenreIds(profileUtils.virtualBandGenres(userDetail.genres)); + $virtualBandGenreList.data('genres', genreIds).html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) genres = profileUtils.traditionalBandGenreList(userDetail.genres) - $traditionalBandGenreList.html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) + var genreIds = profileUtils.getGenreIds(profileUtils.traditionalBandGenres(userDetail.genres)); + $traditionalBandGenreList.data('genres', genreIds).html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) genres = profileUtils.paidSessionGenreList(userDetail.genres) - $paidSessionsGenreList.html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) + var genreIds = profileUtils.getGenreIds(profileUtils.paidSessionGenres(userDetail.genres)); + $paidSessionsGenreList.data('genres', genreIds).html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) genres = profileUtils.freeSessionGenreList(userDetail.genres) - $freeSessionsGenreList.html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) + var genreIds = profileUtils.getGenreIds(profileUtils.freeSessionGenres(userDetail.genres)); + $freeSessionsGenreList.data('genres', genreIds).html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) genres = profileUtils.cowritingGenreList(userDetail.genres) - $cowritingGenreList.html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) + var genreIds = profileUtils.getGenreIds(profileUtils.cowritingGenres(userDetail.genres)); + $cowritingGenreList.data('genres', genreIds).html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) // Column 3 - misc (play commitment, rates, cowriting purpose) $virtualBandCommitment.val(userDetail.virtual_band_commitment) @@ -165,7 +179,7 @@ } ui.launchGenreSelectorDialog(type, genres, function(selectedGenres) { - $genreList.html(selectedGenres && selectedGenres.length > 0 ? selectedGenres.join(GENRE_LIST_DELIMITER) : NONE_SPECIFIED) + $genreList.data('genres', selectedGenres).html(selectedGenres && selectedGenres.length > 0 ? selectedGenres.join(GENRE_LIST_DELIMITER) : NONE_SPECIFIED) }) return false @@ -182,7 +196,7 @@ $btnCancel.click(function(e) { e.stopPropagation() - navigateTo('/client#/profile/' + context.JK.currentUserId) + window.ProfileActions.cancelProfileEdit() return false }) @@ -278,24 +292,24 @@ api.updateUser({ virtual_band: $screen.find('input[name=virtual_band]:checked').val(), - virtual_band_genres: $virtualBandGenreList.html() === NONE_SPECIFIED ? [] : $virtualBandGenreList.html().split(GENRE_LIST_DELIMITER), + virtual_band_genres: $virtualBandGenreList.data('genres'), virtual_band_commitment: $virtualBandCommitment.val(), traditional_band: $screen.find('input[name=traditional_band]:checked').val(), - traditional_band_genres: $traditionalBandGenreList.html() === NONE_SPECIFIED ? [] : $traditionalBandGenreList.html().split(GENRE_LIST_DELIMITER), + traditional_band_genres: $traditionalBandGenreList.data('genres'), traditional_band_commitment: $traditionalBandCommitment.val(), traditional_band_touring: $traditionalTouringOption.val(), paid_sessions: $screen.find('input[name=paid_sessions]:checked').val(), - paid_session_genres: $paidSessionsGenreList.html() === NONE_SPECIFIED ? [] : $paidSessionsGenreList.html().split(GENRE_LIST_DELIMITER), + paid_session_genres: $paidSessionsGenreList.data('genres'), paid_sessions_hourly_rate: profileUtils.normalizeMoneyForSubmit($hourlyRate.val()), paid_sessions_daily_rate: profileUtils.normalizeMoneyForSubmit($dailyRate.val()), free_sessions: $screen.find('input[name=free_sessions]:checked').val(), - free_session_genres: $freeSessionsGenreList.html() === NONE_SPECIFIED ? [] : $freeSessionsGenreList.html().split(GENRE_LIST_DELIMITER), + free_session_genres: $freeSessionsGenreList.data('genres'), cowriting: $screen.find('input[name=cowriting]:checked').val(), - cowriting_genres: $cowritingGenreList.html() === NONE_SPECIFIED ? [] : $cowritingGenreList.html().split(GENRE_LIST_DELIMITER), + cowriting_genres: $cowritingGenreList.data('genres'), cowriting_purpose: $cowritingPurpose.val() }) .done(postUpdateProfileSuccess) @@ -305,7 +319,7 @@ function postUpdateProfileSuccess(response) { $document.triggerHandler(EVENTS.USER_UPDATED, response) - context.location = "/client#/account/profile/samples" + ProfileActions.editProfileNext('samples') } function postUpdateProfileFailure(xhr, textStatus, errorMessage) { diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index 0962d513b..fe9cbd870 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -43,7 +43,6 @@ var $btnBack = parent.find('.account-edit-profile-back') var $btnSubmit = parent.find('.account-edit-profile-submit') - var urlValidator=null var soundCloudValidator=null var reverbNationValidator=null @@ -59,9 +58,18 @@ } function afterShow(data) { + if (window.ProfileStore.solo) { + $btnBack.hide() + $btnSubmit.text('SAVE & RETURN TO PROFILE'); + } + else { + $btnBack.show() + $btnSubmit.text('SAVE & FINISH'); + } + $.when(loadFn()) .done(function(targetPlayer) { - if (targetPlayer && targetPlayer.keys && targetPlayer.keys.length > 0) { + if (targetPlayer) { renderPlayer(targetPlayer) } }) @@ -161,6 +169,10 @@ } function buildNonJamKazamEntry($sampleList, type, source) { + + // remove anything that matches + $sampleList.find('[data-recording-id=' + source.recording_id + ']').remove(); + // TODO: this code is repeated in HTML file var recordingIdAttr = ' data-recording-id="' + source.recording_id + '" '; var recordingUrlAttr = ' data-recording-url="' + source.url + '" '; @@ -207,7 +219,9 @@ $btnCancel.click(function(evt) { evt.stopPropagation(); - navigateTo('/client#/profile/' + context.JK.currentUserId); + + window.ProfileActions.cancelProfileEdit() + return false; }); @@ -334,7 +348,7 @@ function postUpdateProfileSuccess(response) { $document.triggerHandler(EVENTS.USER_UPDATED, response); - context.location = "/client#/profile/" + context.JK.currentUserId; + ProfileActions.doneProfileEdit() } function postUpdateProfileFailure(xhr, textStatus, errorMessage) { @@ -364,7 +378,7 @@ } function formatTitle(title) { - return title && title.length > 30 ? title.substring(0, 30) + "..." : title; + return title; } // This function is a bit of a mess. It was pulled @@ -387,7 +401,7 @@ setTimeout(function() { - urlValidator = new JK.SiteValidator('url', userNameSuccessCallback, userNameFailCallback, parent) + urlValidator = new JK.SiteValidator('url', websiteSuccessCallback, userNameFailCallback, parent) urlValidator.init() soundCloudValidator = new JK.SiteValidator('soundcloud', userNameSuccessCallback, userNameFailCallback, parent) @@ -429,6 +443,12 @@ $inputDiv.append("Invalid username").show(); } + function websiteSuccessCallback($inputDiv) { + $inputDiv.removeClass('error'); + $inputDiv.find('.error-text').remove(); + } + + function soundCloudSuccessCallback($inputDiv) { siteSuccessCallback($inputDiv, soundCloudRecordingValidator, $soundCloudSampleList, 'soundcloud'); } diff --git a/web/app/assets/javascripts/accounts_video_profile.js b/web/app/assets/javascripts/accounts_video_profile.js index 628b1e870..ca5e667ad 100644 --- a/web/app/assets/javascripts/accounts_video_profile.js +++ b/web/app/assets/javascripts/accounts_video_profile.js @@ -12,14 +12,13 @@ }; app.bindScreen('account/video', screenBindings); - var reactElement = React.createElement(window.WebcamViewer, {isVisible: false}); + var reactElement = React.createElement(window.WebcamViewer, {isVisible: false, show_header: true, show_disable: true}); var reactDomNode = $("#account-video-profile .webcam-container").get(0) webcamViewerReact = React.render(reactElement, reactDomNode) } function beforeShow() { - console.log("webcamViewerReact", webcamViewerReact) webcamViewerReact.beforeShow() } diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index 9ed23f89c..778e6ce47 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -40,6 +40,10 @@ //= require jquery.exists //= require jquery.payment //= require jquery.visible +//= require jquery.jstarbox +//= require jquery.inputmask +//= require fingerprint2.min +//= require ResizeSensor //= require classnames //= require reflux //= require howler.core.js @@ -53,6 +57,7 @@ //= require ga //= require utils //= require subscription_utils +//= require profile_utils //= require custom_controls //= require react //= require react_ujs @@ -63,6 +68,7 @@ //= require web/tracking //= require webcam_viewer //= require react-components +//= require playbackControls //= require_directory . //= require_directory ./dialog //= require_directory ./wizard diff --git a/web/app/assets/javascripts/backend_alerts.js b/web/app/assets/javascripts/backend_alerts.js index bf5b1993b..705f9ec29 100644 --- a/web/app/assets/javascripts/backend_alerts.js +++ b/web/app/assets/javascripts/backend_alerts.js @@ -137,6 +137,10 @@ else if(type === ALERT_NAMES.VIDEO_WINDOW_CLOSED) { context.VideoActions.videoWindowClosed() } + else if (type === ALERT_NAMES.VST_CHANGED) { + console.log("VST CHANGED!") + context.ConfigureTracksActions.vstChanged() + } 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/chatPanel.js b/web/app/assets/javascripts/chatPanel.js index 45a13014b..995b636c0 100644 --- a/web/app/assets/javascripts/chatPanel.js +++ b/web/app/assets/javascripts/chatPanel.js @@ -1,8 +1,8 @@ -(function(context,$) { +(function (context, $) { "use strict"; context.JK = context.JK || {}; - context.JK.ChatPanel = function(app) { + context.JK.ChatPanel = function (app) { var logger = context.JK.logger; var rest = context.JK.Rest(); var $panel = null; @@ -18,7 +18,7 @@ var $errorMsg = null; var sendingMessage = false; var showing = false; - var fullyInitialized = false; + var fullyInitialized = true; var renderQueue = []; var sidebar = null; var user = null; @@ -27,11 +27,11 @@ var next = null; function reset() { - fullyInitialized = false; + fullyInitialized = true; renderQueue = []; sendingMessage = false; - $chatMessages.empty(); - $textBox.val(''); + //$chatMessages.empty(); + //$textBox.val(''); } function buildMessage() { @@ -45,28 +45,28 @@ } function sendMessage() { - if(!context.JK.JamServer.connected) { + if (!context.JK.JamServer.connected) { return false; } var msg = $textBox.val(); - if(!msg || msg == '') { + if (!msg || msg == '') { // don't bother the server with empty messages return false; } - if(!sendingMessage) { + if (!sendingMessage) { sendingMessage = true; rest.createChatMessage(buildMessage()) - .done(function() { + .done(function () { $textBox.val(''); renderMessage(msg, user.id, user.name, new Date().toISOString(), true); }) - .fail(function(jqXHR) { + .fail(function (jqXHR) { app.notifyServerError(jqXHR, 'Unable to Send Chat Message'); }) - .always(function() { + .always(function () { sendingMessage = false; }) @@ -84,9 +84,9 @@ sender: senderId == user.id ? 'me' : senderName, sent: sent }; - var txt = $(context._.template($('#template-chat-message').html(), options, { variable: 'data' })); + var txt = $(context._.template($('#template-chat-message').html(), options, {variable: 'data'})); txt.find('.timeago').timeago(); - if(append) { + if (append) { $chatMessages.append(txt); scrollToBottom(); } @@ -96,7 +96,7 @@ } function drainQueue() { - context._.each(renderQueue, function(msg) { + context._.each(renderQueue, function (msg) { renderMessage(msg.message, msg.user_id, msg.user_name, msg.sent, true); }); renderQueue = []; @@ -128,51 +128,46 @@ evt.preventDefault(); pasteIntoInput(this, "\n"); } - else if(evt.keyCode == 13 && !evt.shiftKey){ + else if (evt.keyCode == 13 && !evt.shiftKey) { sendMessage(); return false; } } function events(bind) { - if (bind) { + /**if (bind) { $form.submit(sendMessage); $textBox.keydown(handleEnter); $sendChatMessageBtn.click(sendMessage); } - else { + else { $form.submit(null); $textBox.keydown(null); $sendChatMessageBtn.click(null); - } + }*/ - registerChatMessage(bind); } // called from sidebar when messages come in function chatMessageReceived(payload) { - if(fullyInitialized) { + if (fullyInitialized) { if (isChatPanelVisible()) { - renderMessage(payload.msg, payload.sender_id, payload.sender_name, payload.created_at, true); + } else { highlightCount(); incrementChatCount(); - renderQueue.push({message: payload.msg, user_id: payload.sender_id, user_name: payload.sender_name, sent: payload.created_at}); - context.jamClient.UserAttention(true); } } - else { - renderQueue.push({message: payload.msg, user_id: payload.sender_id, user_name: payload.sender_name, sent: payload.created_at}); - } } function registerChatMessage(bind) { if (bind && bind == true) { - context.JK.JamServer.registerMessageCallback(context.JK.MessageType.CHAT_MESSAGE, function(header, payload) { + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.CHAT_MESSAGE, function (header, payload) { logger.debug("Handling CHAT_MESSAGE msg " + JSON.stringify(payload)); chatMessageReceived(payload); + context.ChatActions.msgReceived(payload); handledNotification(payload); }); @@ -185,45 +180,41 @@ function opened() { lowlightCount(); setCount(0); - drainQueue(); } + function fullyOpened() { + context.ChatActions.fullyOpened() + } + function sessionStarted(e, data) { $sessionId = data.session.id; - + var lessonId = null; + if (data.session.lesson_session) { + lessonId = data.session.lesson_session.id + } // open chat panel - $chatSender.show(); - $chatMessagesScroller.show(); + //$chatSender.show(); + //$chatMessagesScroller.show(); $errorMsg.hide(); $panel.find('.panel-header').trigger('click'); - $panel.on('open', opened); $panel.find('.btn-next-pager').attr('href', '/api/sessions/' + $sessionId + '/chats?page=1'); reset(); - // load previous chat messages - rest.getChatMessages(buildQuery()) - .done(function (response) { - handleChatResponse(response); - - scrollToBottom(true); - showing = true; - fullyInitialized = true; - drainQueue(); - }) - .fail(function (jqXHR) { - app.notifyServerError(jqXHR, 'Unable to Load Session Conversations') - }); + context.ChatActions.sessionStarted($sessionId, lessonId); + showing = true + fullyInitialized = true; + drainQueue(); events(true); } function sessionStopped(e, data) { // open chat panel - $chatSender.hide(); - $chatMessagesScroller.hide(); - $errorMsg.show(); + //$chatSender.hide(); + //$chatMessagesScroller.hide(); + //$errorMsg.show(); reset(); events(false); @@ -251,9 +242,9 @@ } function buildQuery() { - var query = {type: 'CHAT_MESSAGE', music_session: $sessionId, limit:LIMIT, page:currentPage}; + var query = {type: 'CHAT_MESSAGE', music_session: $sessionId, limit: LIMIT, page: currentPage}; - if(next) { + if (next) { query.start = next; } @@ -271,11 +262,11 @@ renderChats(response.chats); - if(response.next == null) { + if (response.next == null) { // if we less results than asked for, end searching $chatMessagesScroller.infinitescroll('pause'); - if(currentPage > 0) { + if (currentPage > 0) { // 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(); @@ -303,10 +294,10 @@ msg: $('

Loading ...
'), img: '/assets/shared/spinner-32.gif' }, - path: function(page) { + path: function (page) { return '/api/sessions/' + $sessionId + '/chats?' + $.param(buildQuery()); } - },function(json, opts) { + }, function (json, opts) { handleChatResponse(json); }); $chatMessagesScroller.infinitescroll('resume'); @@ -318,28 +309,26 @@ $contents = $panel.find('.chatcontents'); $chatMessagesScroller = $panel.find('.chat-list-scroller'); $count = $panel.find('#sidebar-chat-count'); - $chatMessages = $panel.find('.previous-chat-list'); - $sendChatMessageBtn = $panel.find('.btn-send-chat-message'); - $chatSender = $panel.find('.chat-sender'); - $form = $panel.find('.chat-message-form'); - $textBox = $form.find('textarea'); + $errorMsg = $panel.find('.chat-status'); - $errorMsg.show(); - $chatSender.hide(); - $chatMessagesScroller.hide(); + $panel.on('open', opened); + $panel.on('fullyOpen', fullyOpened); app.user() .done(function (userDetail) { user = userDetail; + + registerChatMessage(true); + }); } this.initialize = initialize; this.sessionStarted = sessionStarted; - this.sessionStopped = sessionStopped; + this.sessionStopped = sessionStopped; this.registerChatMessage = registerChatMessage; }; return this; -})(window,jQuery); \ No newline at end of file +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/checkout_complete.js b/web/app/assets/javascripts/checkout_complete.js index 4f1e96110..aa52214b9 100644 --- a/web/app/assets/javascripts/checkout_complete.js +++ b/web/app/assets/javascripts/checkout_complete.js @@ -15,6 +15,7 @@ var $templatePurchasedJamTrack = null; var $thanksPanel = null; var $jamTrackInBrowser = null; + var $giftCardPurchased = null; var $purchasedJamTrack = null; var $purchasedJamTrackHeader = null; var $purchasedJamTracks = null; @@ -75,9 +76,17 @@ else { $thanksPanel.removeClass('hidden') handleJamTracksPurchased(purchaseResponse.jam_tracks) + handleGiftCardsPurchased(purchaseResponse.gift_cards) } } + + function handleGiftCardsPurchased(gift_cards) { + // were any GiftCards purchased? + if(gift_cards && gift_cards.length > 0) { + $giftCardPurchased.removeClass('hidden') + } + } function handleJamTracksPurchased(jamTracks) { // were any JamTracks purchased? var jamTracksPurchased = jamTracks && jamTracks.length > 0; @@ -194,6 +203,7 @@ $templatePurchasedJamTrack = $('#template-purchased-jam-track'); $thanksPanel = $screen.find(".thanks-panel"); $jamTrackInBrowser = $screen.find(".thanks-detail.jam-tracks-in-browser"); + $giftCardPurchased = $screen.find('.thanks-detail.gift-card') $purchasedJamTrack = $thanksPanel.find(".thanks-detail.purchased-jam-track"); $purchasedJamTrackHeader = $purchasedJamTrack.find(".purchased-jam-track-header"); $purchasedJamTracks = $purchasedJamTrack.find(".purchased-list") diff --git a/web/app/assets/javascripts/checkout_order.js b/web/app/assets/javascripts/checkout_order.js index 168b965d6..a69c0a39c 100644 --- a/web/app/assets/javascripts/checkout_order.js +++ b/web/app/assets/javascripts/checkout_order.js @@ -135,15 +135,7 @@ } } - function displayTax(effectiveQuantity, item_tax, total_with_tax) { - var totalTax = 0; - var totalPrice = 0; - - var unitTax = item_tax * effectiveQuantity; - totalTax += unitTax; - - var totalUnitPrice = total_with_tax * effectiveQuantity; - totalPrice += totalUnitPrice; + function displayTax(totalTax, totalPrice) { $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)) @@ -181,8 +173,16 @@ taxRate = 0.0825; } - var unitTax = 1.99 * taxRate; - displayTax(effectiveQuantity, unitTax, 1.99 + unitTax) + var estimatedTax = 0; + var estimatedTotal = 0; + + context._.each(carts, function(cart) { + var cart_quantity = cart.product_info.quantity - cart.product_info.marked_for_redeem + estimatedTax += cart.product_info.price * cart_quantity * taxRate; + estimatedTotal += cart.product_info.price * cart_quantity; + }) + + displayTax(Math.round(estimatedTax*100)/100, Math.round((estimatedTotal + estimatedTax)*100)/100) } else { checkoutUtils.configureRecurly() diff --git a/web/app/assets/javascripts/checkout_payment.js b/web/app/assets/javascripts/checkout_payment.js index 9b72f4fc5..956c11886 100644 --- a/web/app/assets/javascripts/checkout_payment.js +++ b/web/app/assets/javascripts/checkout_payment.js @@ -95,7 +95,7 @@ $reuseExistingCardChk.iCheck(userDetail.reuse_card && userDetail.has_recurly_account ? 'check' : 'uncheck').attr('checked', userDetail.reuse_card) // show appropriate prompt text based on whether user has a free jamtrack - if(user.free_jamtrack) { + if(user.has_redeemable_jamtrack) { $freeJamTrackPrompt.removeClass('hidden') } else { diff --git a/web/app/assets/javascripts/checkout_utils.js.coffee b/web/app/assets/javascripts/checkout_utils.js.coffee index c16e09f9b..c789ba71d 100644 --- a/web/app/assets/javascripts/checkout_utils.js.coffee +++ b/web/app/assets/javascripts/checkout_utils.js.coffee @@ -55,6 +55,16 @@ class CheckoutUtils return carts[0].product_info.free + hasOnlyFreeItemsInShoppingCart: (carts) => + if carts.length == 0 + return false + + for cart in carts + if !cart.product_info.free + return false + + return true + configureRecurly: () => unless @configuredRecurly context.recurly.configure(gon.global.recurly_public_api_key) diff --git a/web/app/assets/javascripts/clientUpdate.js b/web/app/assets/javascripts/clientUpdate.js index ca5bdd428..dd3142e76 100644 --- a/web/app/assets/javascripts/clientUpdate.js +++ b/web/app/assets/javascripts/clientUpdate.js @@ -61,7 +61,10 @@ }) } - app.layout.showDialog('client-update') + if(!app.layout.isDialogShowing('client-update')) { + app.layout.showDialog('client-update') + } + //$('#client_update').show() //$('#client_update_overlay').show() } @@ -192,6 +195,11 @@ function runCheck(product, version, uri, size, currentVersion) { + if (app.clientUpdating) { + logger.debug("client is already updating; skipping") + return + } + if(currentVersion === undefined) { currentVersion = context.jamClient.ClientUpdateVersion(); @@ -302,7 +310,7 @@ $(document).on(EVENTS.SESSION_ENDED, function(e, data){ if(app.clientUpdating) { - updateClientUpdateDialog("update-start", { uri: updateUri }) + updateClientUpdateDialog("update-start", { uri: updateUri}) } }); diff --git a/web/app/assets/javascripts/client_init.js.coffee b/web/app/assets/javascripts/client_init.js.coffee index c84859b69..b4cb033a2 100644 --- a/web/app/assets/javascripts/client_init.js.coffee +++ b/web/app/assets/javascripts/client_init.js.coffee @@ -3,7 +3,6 @@ $ = jQuery context = window context.JK ||= {}; -broadcastActions = @BroadcastActions logger = context.JK.logger context.JK.ClientInit = class ClientInit @@ -20,8 +19,11 @@ context.JK.ClientInit = class ClientInit this.watchBroadcast() + if context.jamClient.RegisterSessionJoinLeaveRequestCallBack? + context.jamClient.RegisterSessionJoinLeaveRequestCallBack("SessionStore.handleJoinLeaveRequestCallback") + checkBroadcast: () => - promise = broadcastActions.load.trigger() + promise = window.BroadcastActions.load.trigger() if promise promise.catch(() -> false diff --git a/web/app/assets/javascripts/configureTracksHelper2.js b/web/app/assets/javascripts/configureTracksHelper2.js new file mode 100644 index 000000000..d87319a53 --- /dev/null +++ b/web/app/assets/javascripts/configureTracksHelper2.js @@ -0,0 +1,294 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.ConfigureTracksHelper2 = function (app) { + var logger = context.JK.logger; + var ASSIGNMENT = context.JK.ASSIGNMENT; + var VOICE_CHAT = context.JK.VOICE_CHAT; + var MAX_TRACKS = context.JK.MAX_TRACKS; + var MAX_OUTPUTS = context.JK.MAX_OUTPUTS; + var gearUtils = context.JK.GearUtils; + + var $parent = null; + var $templateAssignablePort = null; + var $templateTrackTarget = null; + var $templateOutputTarget = null; + var $unassignedInputsHolder = null; + var $unassignedOutputsHolder = null; + var $tracksHolder = null; + var $outputChannelHolder = null; + var $instrumentsHolder = null; + var isDragging = false; + + + + // inputChannelFilter is an optional argument that is used by the Gear Wizard. + // basically, if an input channel isn't in there, it's not going to be displayed + function loadChannels(forceInputsToUnassign, inputChannelFilter) { + var musicPorts = jamClient.FTUEGetChannels(); + + $unassignedInputsHolder.empty(); + $unassignedOutputsHolder.empty(); + + var inputChannels = musicPorts.inputs; + var outputChannels = musicPorts.outputs; + + // uncomment to add a bunch of bogus channels + //inputChannels = inputChannels.concat(inputChannels.concat(inputChannels.concat(inputChannels))) + + context._.each(inputChannels, function (inputChannel) { + + if(inputChannelFilter && !(inputChannelFilter.indexOf(inputChannel.id) > -1)) { + // skipping input channel because it's not in the filter + return; + } + + var $channel = $(context._.template($templateAssignablePort.html(), $.extend({}, inputChannel, {direction:'in'}), { variable: 'data' })); + + if(forceInputsToUnassign || inputChannel.assignment == ASSIGNMENT.UNASSIGNED) { + unassignInputChannel($channel); + } + else if(inputChannel.assignment == ASSIGNMENT.CHAT) { + // well, we can't show it as unused... if there were a place to show chat inputs, we would put it there. + // but we don't have it, so just skip + logger.debug("skipping chat channel ", inputChannel) + return; + } + else { + // find the track this belongs in + + var trackNumber = inputChannel.assignment - 1; + + // plop down a track holder on the UI + + addChannelToTrack($channel, trackNumber); + } + }) + + var outputAssignment = 0; + context._.each(outputChannels, function (outputChannel, index) { + var $channel = $(context._.template($templateAssignablePort.html(), $.extend({}, outputChannel, {direction:'out'}), { variable: 'data' })); + + + + if(outputChannel.assignment == ASSIGNMENT.UNASSIGNED) { + unassignOutputChannel($channel); + } + else { + var $output = $outputChannelHolder.find('.output[data-num="' + outputAssignment + '"]'); + outputAssignment++; + + if($output.length == 0) { + context.JK.alertSupportedNeeded('Unable to find an output holder for channel: ' + outputChannel.id + ' with assignment ' + outputChannel.assignment); + return false; + } + addChannelToOutput($channel, $output.find('.output-target')); + } + }); + } + + // iterates through the dom and returns a pure data structure for track associations and output channels + function getCurrentState() { + + var state = {}; + state.tracks = []; + state.unassignedChannels = []; + state.outputs = []; + var $unassignedInputChannels = $unassignedInputsHolder.find('.ftue-input'); + var $unassignedOutputChannels = $unassignedOutputsHolder.find('.ftue-input'); + var $tracks = $tracksHolder.find('.track-target'); + var $outputs = $outputChannelHolder.find('.output-target'); + + context._.each($unassignedInputChannels, function($unassignedInput) { + $unassignedInput = $($unassignedInput); + var channelId = $unassignedInput.attr('data-input-id'); + state.unassignedChannels.push(channelId); + }) + + context._.each($unassignedOutputChannels, function($unassignedOutput) { + $unassignedOutput = $($unassignedOutput); + var channelId = $unassignedOutput.attr('data-input-id'); + state.unassignedChannels.push(channelId); + }) + + context._.each($tracks, function($track, index) { + $track = $($track); + var $assignedChannels = $track.find('.ftue-input'); + + var track = {index: index, channels:[]}; + context._.each($assignedChannels, function($assignedChannel) { + $assignedChannel = $($assignedChannel); + track.channels.push($assignedChannel.attr('data-input-id')) + }); + + // sparse array + if(track.channels.length > 0) { + state.tracks.push(track); + } + var $instrument = $instrumentsHolder.find('[data-num="' + index + '"]').find('.icon-instrument-select'); + track.instrument_id = $instrument.data('instrument_id'); + }) + + context._.each($outputs, function($output, index) { + $output = $($output); + var $assignedChannel = $output.find('.ftue-input'); + + // this is overkill since there should only be 1 or 0 .ftue-inputs in a given .output + var outputSlot = {index: index, channels:[]}; + context._.each($assignedChannel, function($assignedChannel) { + $assignedChannel = $($assignedChannel); + outputSlot.channels.push($assignedChannel.attr('data-input-id')) + }); + + // sparse array + if(outputSlot.channels.length > 0) { + state.outputs.push(outputSlot); + } + }) + return state; + } + + function validate(tracks) { + // there must be at least one assigned channel + if(tracks.tracks.length == 0) { + logger.debug("ConfigureTracks validation error: must have assigned at least one input port to a track."); + context.JK.Banner.showAlert('Must have assigned at least one input port to a track.'); + return false; + } + + // there must be some instruments + context._.each(tracks.tracks, function(track) { + if(!track.instrument_id) { + logger.debug("ConfigureTracks validation error: all tracks with ports assigned must specify an instrument."); + context.JK.Banner.showAlert('Please use the instrument icons to choose what you plan to play on each track.'); + return false; + } + }); + + // there must be exactly 2 output channels assigned + if(tracks.outputs.length != 2 || (tracks.outputs[0].channels.length != 1 && track.outputs[1].channels.length != 1)) { + logger.debug("ConfigureTracks validation error: must have assigned exactly two output ports"); + context.JK.Banner.showAlert('Must have assigned exactly 2 output ports.'); + return false; + } + + return true; + } + + function save(state) { + + context._.each(state.unassignedChannels, function(unassignedChannelId) { + context.jamClient.TrackSetAssignment(unassignedChannelId, true, ASSIGNMENT.UNASSIGNED); + }); + + // save input/tracks + context._.each(state.tracks, function(track, index) { + + var trackNumber = index + 1; + + context._.each(track.channels, function(channelId) { + context.jamClient.TrackSetAssignment(channelId, true, trackNumber); + + }); + logger.debug("context.jamClient.TrackSetInstrument(trackNumber, track.instrument_id)", trackNumber, track.instrument_id); + context.jamClient.TrackSetInstrument(trackNumber, context.JK.instrument_id_to_instrument[track.instrument_id].client_id); + }); + + // save outputs + context._.each(state.outputs, function(output, index) { + context._.each(output.channels, function(channelId) { + context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.OUTPUT); + }); + }); + + var result = context.jamClient.TrackSaveAssignments(); + + if(!result || result.length == 0) { + // success + return true; + } + else { + context.JK.Banner.showAlert('Unable to save assignments. ' + result); + return false; + } + } + + function loadTrackInstruments(forceInputsToUnassign) { + var $trackInstruments = $instrumentsHolder.find('.track-instrument'); + + context._.each($trackInstruments, function(trackInstrument) { + var $trackInstrument = $(trackInstrument); + + var trackIndex = parseInt($trackInstrument.attr('data-num')) + 1; + + var clientInstrument = context.jamClient.TrackGetInstrument(trackIndex); + + var instrument = context.JK.client_to_server_instrument_map[clientInstrument]; + + $trackInstrument.instrumentSelectorSet(instrument ? instrument.server_id : instrument); + }); + } + + function trySave() { + var state = getCurrentState(); + + if(!validate(state)) { + return false; + } + + var saved = save(state); + + if(saved) { + context.JK.GA.trackConfigureTracksCompletion(context.JK.detectOS()); + } + + return saved; + } + + function reset(forceInputsToUnassign, inputChannelFilter) { + loadChannels(forceInputsToUnassign, inputChannelFilter); + loadTrackInstruments(forceInputsToUnassign); + } + + function unassignOutputChannel($channel) { + var $originallyAssignedTrack = $channel.closest('.output-target'); + $unassignedOutputsHolder.append($channel); + $originallyAssignedTrack.attr('output-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length); + } + + function unassignInputChannel($channel) { + var $originallyAssignedTrack = $channel.closest('.track-target'); + $unassignedInputsHolder.append($channel); + $originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length); + + } + + function addChannelToTrack($channel, $track) { + var $originallyAssignedTrack = $channel.closest('.track-target'); + $track.append($channel); + $track.attr('track-count', $track.find('.ftue-input:not(.ui-draggable-dragging)').length); + $originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length) + } + + function addChannelToOutput($channel, $slot) { + var $originallyAssignedTrack = $channel.closest('.output-target'); + $slot.append($channel); + $slot.attr('output-count', $slot.find('.ftue-input:not(.ui-draggable-dragging)').length); + $originallyAssignedTrack.attr('output-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length) + } + + function initialize(_$parent) { + $parent = _$parent; + + } + + this.initialize = initialize; + this.trySave = trySave; + this.reset = reset; + + return this; + }; + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/dialog/banner.js b/web/app/assets/javascripts/dialog/banner.js index 2c777c0f0..cb6e454bb 100644 --- a/web/app/assets/javascripts/dialog/banner.js +++ b/web/app/assets/javascripts/dialog/banner.js @@ -16,6 +16,9 @@ var $noShow = null; var $noShowCheckbox = null; var $buttons = null; + var $alertIcon = null; + var $noticeIcon = null; + // you can also do // * showAlert('title', 'text') @@ -46,6 +49,20 @@ return show(options); } + function showNotice(options) { + if (typeof options == 'string' || options instanceof String) { + if(arguments.length == 2) { + options = {title: options, html:arguments[1]} + } + else { + options = {html:options}; + } + } + options.type = 'notice' + return show(options); + } + + // responsible for updating the contents of the update dialog // as well as registering for any event handlers function show(options) { @@ -59,6 +76,9 @@ else if(options.type == 'yes_no') { options.title = 'please confirm'; } + else if(options.type == 'notice') { + options.title = 'notice'; + } } hide(); @@ -79,9 +99,17 @@ throw "unable to show banner for empty message"; } + if(options.type == "notice") { + $alertIcon.hide() + $noticeIcon.show() + } + else { + $alertIcon.show() + $noticeIcon.hide() + } - if((options.type == "alert" && !options.buttons) || options.close || options.no_show) { + if(((options.type == "alert" || options.type == "notice") && !options.buttons) || options.close || options.no_show) { var closeButtonText = 'CLOSE'; if(options.close !== null && typeof options.close == 'object') { @@ -92,7 +120,6 @@ } if(options.no_show) { - $buttons.addClass('center') $noShowCheckbox.data('no_show', options.no_show) $noShow.show() } @@ -110,7 +137,8 @@ } if(options.type == "yes_no") { - $yesBtn.show().unbind('click').click(function() { + var yesText = options.yes_text || 'YES' + $yesBtn.text(yesText).show().unbind('click').click(function() { if(options.yes) { options.yes(); } @@ -133,7 +161,7 @@ if(options.buttons) { context._.each(options.buttons, function(button, i) { if(!button.name) throw "button.name must be specified"; - if(!button.click && !button.href) throw "button.click or button.href must be specified"; + //if(!button.click && !button.href) throw "button.click or button.href must be specified"; var buttonStyle = button.buttonStyle; if(!buttonStyle) { @@ -147,7 +175,10 @@ } else { $btn.click(function() { - button.click(); + if (button.click) { + button.click(); + } + hide(); return false; }); @@ -169,7 +200,6 @@ $banner.find('.user-btn').remove(); $('#banner_overlay .dialog-inner').html(""); $('#banner_overlay').hide(); - $buttons.removeClass('center') $noShowCheckbox.data('no_show', null).iCheck('uncheck').attr('checked', false) $buttons.children().hide(); } @@ -184,6 +214,8 @@ $noShowCheckbox = $banner.find('.no-more-show-checkbox') $noShow = $banner.find('.no-more-show') $buttons = $banner.find('.buttons') + $alertIcon = $banner.find('.content-icon.alert') + $noticeIcon = $banner.find('.content-icon.notice') context.JK.checkbox($noShowCheckbox); return self; @@ -195,6 +227,7 @@ show: show, showAlert: showAlert, showYesNo: showYesNo,// shows Yes and Cancel button (confirmation dialog) + showNotice: showNotice, hide: hide } diff --git a/web/app/assets/javascripts/dialog/clientPreferencesDialog.js b/web/app/assets/javascripts/dialog/clientPreferencesDialog.js index 9b4f0fa72..20bbf217e 100644 --- a/web/app/assets/javascripts/dialog/clientPreferencesDialog.js +++ b/web/app/assets/javascripts/dialog/clientPreferencesDialog.js @@ -11,17 +11,76 @@ var $dialog = null; var $autoStartField = null; var $autoStartInput = null; + var $useStaticPortField = null; + var $useStaticPortInput = null; + var $staticPortField = null; + var $staticPortInput = null; var $btnSave = null; + var beforeValues = {} + + function validate() { + + var staticPort = $staticPortInput.val(); + + staticPort = new Number(staticPort); + + console.log("staticPort", staticPort) + if(context._.isNaN(staticPort)) { + app.layout.notify({title: 'No Settings Have Been Saved!', text: 'Please enter a number from 1026-49150.'}) + return false; + } + + /** + if (staticPort % 2 == 1) { + app.layout.notify({title: 'No Settings Have Been Saved!', text: 'Please pick an even port number.'}) + return false; + } + */ + + if (staticPort < 1026 || staticPort >= 65525) { + app.layout.notify({title: 'No Settings Have Been Saved!', text: 'Please pick a port from 1026 to 65524.'}) + return false; + } + + return true; + } + function events() { context.JK.checkbox($autoStartInput); + context.JK.checkbox($useStaticPortInput); $btnSave.click(function() { + if (!validate()) { + return false; + } + var autostart = $autoStartField.find('.icheckbox_minimal').is('.checked'); context.jamClient.SetAutoStart(autostart); + + var useStaticPort = $useStaticPortField.find('.icheckbox_minimal').is('.checked'); + context.jamClient.SetUseStaticPort(useStaticPort); + + var staticPort = new Number($staticPortInput.val()); + context.jamClient.SetStaticPort(staticPort); + app.layout.closeDialog('client-preferences-dialog') context.jamClient.SaveSettings(); + + logger.debug("New Client Settings", {autostart: autostart, useStaticPort: useStaticPort, staticPort: staticPort}) + if ((beforeValues.useStaticPort != useStaticPort) || (beforeValues.staticPort != staticPort)) { + context.JK.Banner.showYesNo({ + title: "Please Restart", + html: "The changes you made won't take effect until you restart JamKazam. Restart now?", + yes: function() { + context.jamClient.RestartApplication(); + }, + no : function() { + context.JK.Banner.hide(); + } + }) + } return false; }) } @@ -29,6 +88,16 @@ function beforeShow() { var autostart = context.jamClient.GetAutoStart(); autostart ? $autoStartInput.iCheck('check') : $autoStartInput.iCheck('uncheck'); + + var useStaticPort = context.jamClient.GetUseStaticPort(); + useStaticPort ? $useStaticPortInput.iCheck('check') : $useStaticPortInput.iCheck('uncheck'); + + var staticPort = context.jamClient.GetStaticPort(); + + $staticPortInput.val(staticPort); + + beforeValues = {autostart: autostart, useStaticPort: useStaticPort, staticPort: staticPort} + logger.debug("Client Settings:", beforeValues) } function afterHide() { @@ -47,6 +116,10 @@ $dialog = $(dialogId); $autoStartField = $dialog.find('.field[data-purpose="autostart"]') $autoStartInput = $dialog.find('input[name="autostart"]') + $useStaticPortField = $dialog.find('.field[data-purpose="use-static-port"]') + $useStaticPortInput = $dialog.find('input[name="use-static-port"]') + $staticPortField = $dialog.find('.field[data-purpose="static-port"]') + $staticPortInput = $dialog.find('input[name="static-port"]') $btnSave = $dialog.find('.btnSave') events(); diff --git a/web/app/assets/javascripts/dialog/configureTrackDialog.js b/web/app/assets/javascripts/dialog/configureTrackDialog.js index 20801cfa6..c7b908fbc 100644 --- a/web/app/assets/javascripts/dialog/configureTrackDialog.js +++ b/web/app/assets/javascripts/dialog/configureTrackDialog.js @@ -25,6 +25,7 @@ var voiceChatHelper = null; var profiles = null; var currentProfile = null; + var enableVstTimeout = null; var configure_audio_instructions = { @@ -50,7 +51,7 @@ function setInstructions(type) { if (type === 'audio') { - $instructions.html('Choose your audio device. Drag and drop to assign input ports to tracks, and specify the instrument for each track. Drag and drop to assign a pair of output ports for session stereo audio monitoring.') + $instructions.html("Click the 'ADD LIVE TRACK' button to add more tracks. You may set up a live track for each instrumental and/or vocal part to perform in sessions. You must also set up exactly two Session Audio Output ports to deliver the stereo audio in your sessions.") return; var os = context.jamClient.GetOSAsString(); $instructions.html(configure_audio_instructions[os]); @@ -91,7 +92,7 @@ } function validateAudioSettings() { - return configureTracksHelper.trySave(); + return true; } function showVoiceChatPanel() { @@ -103,7 +104,7 @@ $musicAudioTabSelector.click(function () { // validate voice chat settings if (validateVoiceChatSettings()) { - configureTracksHelper.reset(); + window.ConfigureTracksActions.reset(false); voiceChatHelper.reset(); showMusicAudioPanel(); } @@ -113,7 +114,7 @@ // validate audio settings if (validateAudioSettings()) { logger.debug("initializing voice chat helper") - configureTracksHelper.reset(); + window.ConfigureTracksActions.reset(false); voiceChatHelper.reset(); showVoiceChatPanel(); } @@ -127,13 +128,14 @@ return false; }); - //$btnAddNewGear.click(function() { + $btnAddNewGear.click(function() { - // return false; - //}); + app.layout.showDialog("add-new-audio-gear") + return false; + }); $btnUpdateTrackSettings.click(function() { - if(configureTracksHelper.trySave() && voiceChatHelper.trySave()) { + if(voiceChatHelper.trySave()) { app.layout.closeDialog('configure-tracks'); } @@ -152,7 +154,7 @@ }); $certifiedAudioProfile.html(optionsHtml); - context.JK.dropdown($certifiedAudioProfile); + //context.JK.dropdown($certifiedAudioProfile); } function deviceChanged() { @@ -183,7 +185,7 @@ currentProfile = profile; - configureTracksHelper.reset(); + window.ConfigureTracksActions.reset(false); } function beforeShow() { @@ -207,13 +209,47 @@ return; } - configureTracksHelper.reset(); + window.ConfigureTracksActions.reset(false); + if(!window.SessionStore.inSession()) { + delayEnableVst() + } + else { + logger.debug("in a session, so no delayEnableVst()"); + } voiceChatHelper.reset(); voiceChatHelper.beforeShow(); } + function delayEnableVst() { + if (enableVstTimeout) { + clearTimeout(enableVstTimeout) + } + var isVstLoaded = context.jamClient.IsVstLoaded() + var hasVstAssignment = context.jamClient.hasVstAssignment() + + if (hasVstAssignment && !isVstLoaded) { + enableVstTimeout = setTimeout(function() { enableVst() }, 1000) + } + } + + function enableVst () { + enableVstTimeout = null + + if (app.layout.isDialogShowing('configure-tracks')) { + ConfigureTracksActions.enableVst() + } + else { + logger.debug("no longer in configure tracks dialog; not enabling VSTs at this time") + } + } + function afterShow() { sessionUtils.SessionPageEnter(); + + window.JamBlasterActions.resyncBonjour() + + //context.ConfigureTracksActions.vstScan(); + } function onCancel() { @@ -238,17 +274,17 @@ $dialog = $('#configure-tracks-dialog'); $instructions = $dialog.find('.instructions span'); - $musicAudioTab = $dialog.find('div[tab-id="music-audio"]'); + $musicAudioTab = $dialog.find('div[data-tab-id="music-audio"]'); $musicAudioTabSelector = $dialog.find('.tab-configure-audio'); - $voiceChatTab = $dialog.find('div[tab-id="voice-chat"]'); + $voiceChatTab = $dialog.find('div[data-tab-id="voice-chat"]'); $voiceChatTabSelector = $dialog.find('.tab-configure-voice'); $certifiedAudioProfile = $dialog.find('.certified-audio-profile'); $btnCancel = $dialog.find('.btn-cancel'); $btnAddNewGear = $dialog.find('.btn-add-new-audio-gear'); $btnUpdateTrackSettings = $dialog.find('.btn-update-settings'); - configureTracksHelper = new context.JK.ConfigureTracksHelper(app); - configureTracksHelper.initialize($dialog); + //configureTracksHelper = new context.JK.ConfigureTracksHelper(app); + //configureTracksHelper.initialize($dialog); voiceChatHelper = new context.JK.VoiceChatHelper(app); voiceChatHelper.initialize($dialog, 'configure_track_dialog', true, {vuType: "vertical", lightCount: 10, lightWidth: 3, lightHeight: 17}, 191); diff --git a/web/app/assets/javascripts/dialog/deleteVideoConfirmDialog.js b/web/app/assets/javascripts/dialog/deleteVideoConfirmDialog.js new file mode 100644 index 000000000..8b6c06811 --- /dev/null +++ b/web/app/assets/javascripts/dialog/deleteVideoConfirmDialog.js @@ -0,0 +1,69 @@ +(function (context, $) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.DeleteVideoConfirmDialog = function (app) { + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var recordingId = null; + var $dialog = null; + var $deleteFromDiskChkBox = null; + var $deleteBtn = null; + var deleting = false; + function resetForm() { + + } + + function beforeShow(args) { + + recordingId = args.d1; + + if(!recordingId) throw "recordingId must be specified"; + + $dialog.data('result', null); + deleting = false; + + } + + function afterHide() { + + } + + function attemptDelete() { + if(deleting) return; + + deleting = true; + + rest.deleteRecordingVideoData(recordingId) + .done(function(){ + $dialog.data('result', true); + app.layout.closeDialog('delete-video-confirm-dialog'); + }) + .fail(app.ajaxError) + } + + function events() { + $deleteBtn.click(attemptDelete); + } + + + function initialize() { + var dialogBindings = { + 'beforeShow': beforeShow, + 'afterHide': afterHide + }; + + app.bindDialog('delete-video-confirm-dialog', dialogBindings); + + $dialog = $('#deleteVideoConfirmDialog'); + $deleteFromDiskChkBox = $dialog.find('.delete-from-disk'); + $deleteBtn = $dialog.find('.delete-btn'); + + events(); + + context.JK.checkbox($deleteFromDiskChkBox); + }; + + this.initialize = initialize; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/dialog/editRecordingDialog.js b/web/app/assets/javascripts/dialog/editRecordingDialog.js index 9aac9d33a..409318c0a 100644 --- a/web/app/assets/javascripts/dialog/editRecordingDialog.js +++ b/web/app/assets/javascripts/dialog/editRecordingDialog.js @@ -14,7 +14,7 @@ var $isPublic = null; var $saveBtn = null; var $deleteBtn = null; - + var $videoField = null; var updating = false; var deleting = false; diff --git a/web/app/assets/javascripts/dialog/genreSelectorDialog.js b/web/app/assets/javascripts/dialog/genreSelectorDialog.js index 3a9bf4c4f..c119cfb06 100644 --- a/web/app/assets/javascripts/dialog/genreSelectorDialog.js +++ b/web/app/assets/javascripts/dialog/genreSelectorDialog.js @@ -28,7 +28,14 @@ checked = 'checked'; } - $genres.append('' + val.description); + var $input = $('') + $input.val(val.id) + if(checked == 'checked') { + $input.attr('checked', 'checked') + } + + $genres.append($input); + $genres.append(val.description); $genres.append(''); }); } diff --git a/web/app/assets/javascripts/dialog/gettingStartedDialog.js b/web/app/assets/javascripts/dialog/gettingStartedDialog.js index 867a411f2..aa35e80e7 100644 --- a/web/app/assets/javascripts/dialog/gettingStartedDialog.js +++ b/web/app/assets/javascripts/dialog/gettingStartedDialog.js @@ -50,7 +50,7 @@ $browserJamTrackBtn.click(function() { app.layout.closeDialog('getting-started') - window.location = '/client#/jamtrack/search' + window.location = '/client#/jamtrack' return false; }) @@ -69,9 +69,9 @@ function beforeShow() { app.user().done(function(user) { - var jamtrackRule = user.free_jamtrack ? 'has-free-jamtrack' : 'no-free-jamtrack' + var jamtrackRule = user.has_redeemable_jamtrack ? 'has-free-jamtrack' : 'no-free-jamtrack' $jamTrackSection.removeClass('has-free-jamtrack').removeClass('no-free-jamtrack').addClass(jamtrackRule) - if(user.free_jamtrack) { + if(user.has_redeemable_jamtrack) { $jamTracksLimitedTime.removeClass('hidden') } }) diff --git a/web/app/assets/javascripts/dialog/openJamTrackDialog.js b/web/app/assets/javascripts/dialog/openJamTrackDialog.js index 8f58b0728..ed400d8c1 100644 --- a/web/app/assets/javascripts/dialog/openJamTrackDialog.js +++ b/web/app/assets/javascripts/dialog/openJamTrackDialog.js @@ -103,9 +103,8 @@ sampleRate = context.jamClient.GetSampleRate() sampleRateForFilename = sampleRate == 48 ? '48' : '44'; doSearch(); - - } + function afterHide() { showing = false; } diff --git a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js index cceaaedc2..97d746144 100644 --- a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js +++ b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js @@ -8,11 +8,29 @@ var playbackControls = null; var recording = null; // deferred object var $dialog = null; + var $saveVideoCheckbox = null + var $uploadToYoutube = null function resetForm() { // remove all display errors $('#recording-finished-dialog form .error-text').remove() $('#recording-finished-dialog form .error').removeClass("error") + if(recording.video) { + if(recording.owner.id == context.JK.currentUserId) { + // only the owner of the video gets to see video options + $dialog.find('.save-video').show() + $dialog.find('.upload-to-youtube').show() + } + else { + $dialog.find('.save-video').hide() + $dialog.find('.upload-to-youtube').hide() + } + + } + else { + $dialog.find('.save-video').hide() + $dialog.find('.upload-to-youtube').hide() + } removeGoogleLoginErrors() } @@ -103,6 +121,20 @@ } function afterHide() { + if(recording && recording.video) { + var name = $('#recording-finished-dialog form input[name=name]').val(); + name = name.replace(/[^A-Za-z0-9\-\ ]/g, ''); + + + var saveToDisk = $('#recording-finished-dialog form input[name=save_video]').is(':checked') + var keepResult = $dialog.data('result'); + keepResult = keepResult && keepResult.keep + + logger.debug("VideoDecision rid:" + recording.id + ", name=" + name + ", keepResult=" + keepResult + ", saveToDisk=" + saveToDisk); + + context.jamClient.VideoDecision(recording.id, name, keepResult && saveToDisk) + } + recording = null; playbackControls.stopMonitor(); context.jamClient.ClosePreviewRecording(); @@ -141,6 +173,7 @@ window._oauth_callback = function() { window._oauth_win.close() + logger.debug("closing window") setGoogleAuthState() } return false; @@ -152,6 +185,8 @@ var upload_to_youtube = $('#recording-finished-dialog form input[name=upload_to_youtube]').is(':checked') + upload_to_youtube = false // don't prevent user from getting through dialog because popup now handles auth + if (upload_to_youtube) { $.ajax({ type: "GET", @@ -182,6 +217,9 @@ var save_video = $('#recording-finished-dialog form input[name=save_video]').is(':checked') var upload_to_youtube = $('#recording-finished-dialog form input[name=upload_to_youtube]').is(':checked') + var recording_id = recording.id + var recording_video = recording.video + rest.claimRecording({ id: recording.id, name: name, @@ -195,6 +233,11 @@ $dialog.data('result', {keep:true}); app.layout.closeDialog('recordingFinished'); context.JK.GA.trackMakeRecording(); + if(recording_video && save_video && upload_to_youtube) { + // you have to have elected to save video to have upload to youtube have + context.VideoUploaderActions.showUploader(recording_id); + } + }) .fail(function (jqXHR) { if (jqXHR.status == 422) { @@ -281,9 +324,9 @@ // Check for google authorization using AJAX and show/hide the // google login button / "signed in" label as appropriate: - $(window).on('focus', function() { + /**$(window).on('focus', function() { setGoogleAuthState(); - }); + }); */ } function setGoogleAuthState() { @@ -318,10 +361,24 @@ } function initializeButtons() { + $saveVideoCheckbox = $('#recording-finished-dialog input[name="save_video"]') + $uploadToYoutube = $('#recording-finished-dialog input[name="upload_to_youtube"]') var isPublic = $('#recording-finished-dialog input[name="is_public"]'); context.JK.checkbox(isPublic); - context.JK.checkbox($('#recording-finished-dialog input[name="save_video"]')); - context.JK.checkbox($('#recording-finished-dialog input[name="upload_to_youtube"]')); + context.JK.checkbox($saveVideoCheckbox); + context.JK.checkbox($uploadToYoutube); + + $saveVideoCheckbox.on('ifChanged', function() { + var saveVideoToDisk = $saveVideoCheckbox.is(':checked') + + if(saveVideoToDisk) { + $uploadToYoutube.iCheck('enable') + } + else { + $uploadToYoutube.iCheck('disable') + } + + }) } function initialize() { diff --git a/web/app/assets/javascripts/dialog/recordingSelectorDialog.js b/web/app/assets/javascripts/dialog/recordingSelectorDialog.js index 10e76595b..d34a338f5 100644 --- a/web/app/assets/javascripts/dialog/recordingSelectorDialog.js +++ b/web/app/assets/javascripts/dialog/recordingSelectorDialog.js @@ -15,9 +15,10 @@ var feedHelper = new context.JK.Feed(app); var $scroller = $recordings; var $content = $recordings; + var $recordsHolder = $screen.find('.recordings-content'); var $noMoreFeeds = $screen.find('.end-of-list'); var $empty = $(); - feedHelper.initialize($screen, $scroller, $content, $noMoreFeeds, $empty, $empty, $empty, $empty, {sort: 'date', time_range: 'all', type: 'recording', show_checkbox: true, hide_avatar: true}); + feedHelper.initialize($screen, $scroller, $recordsHolder, $noMoreFeeds, $empty, $empty, $empty, $empty, {sort: 'date', time_range: 'all', type: 'recording', show_checkbox: true, hide_avatar: true}); function beforeShow(data) { diff --git a/web/app/assets/javascripts/dialog/sessionSettingsDialog.js b/web/app/assets/javascripts/dialog/sessionSettingsDialog.js index 960d44cfd..92e577ecf 100644 --- a/web/app/assets/javascripts/dialog/sessionSettingsDialog.js +++ b/web/app/assets/javascripts/dialog/sessionSettingsDialog.js @@ -66,6 +66,20 @@ $('#session-settings-fan-access').val('listen-chat-band'); } + var $controllerSelect = $('#session-settings-master-mix-controller') + + $controllerSelect.empty() + var sessionUsers = context.SessionStore.helper.users() + + $controllerSelect.append('') + $.each(sessionUsers, function(userId, user) { + var selected = currentSession.session_controller_id == userId ? 'selected="selected"' : '' + $controllerSelect.append('') + }) + + var canEditController = currentSession.session_controller_id == context.JK.currentUserId || context.JK.currentUserId == currentSession.user_id + $controllerSelect.easyDropDown(canEditController ? 'enable' : 'disable') + /** // notation files in the account screen. ugh. $selectedFilenames.empty(); @@ -84,10 +98,12 @@ context.JK.dropdown($('#session-settings-language')); context.JK.dropdown($('#session-settings-musician-access')); context.JK.dropdown($('#session-settings-fan-access')); + context.JK.dropdown($('#session-settings-master-mix-controller')); var easyDropDownState = canPlayWithOthers.canPlay ? 'enable' : 'disable' $('#session-settings-musician-access').easyDropDown(easyDropDownState) $('#session-settings-fan-access').easyDropDown(easyDropDownState) + } function addNotation(notation) { @@ -121,6 +137,7 @@ data.name = $('#session-settings-name').val(); data.description = $('#session-settings-description').val(); data.language = $('#session-settings-language').val(); + data.session_controller = $('#session-settings-master-mix-controller').val() // musician access var musicianAccess = $('#session-settings-musician-access').val(); @@ -148,7 +165,13 @@ data.fan_chat = true; } - rest.updateSession($('#session-settings-id').val(), data).done(settingsSaved); + rest.updateSession($('#session-settings-id').val(), data).done(settingsSaved) + .done(function(response) { + context.SessionActions.updateSession.trigger(response); + }) + .fail(function() { + app.notify({title: "Can't Update", text: "Unable to update session settings."}) + }) return false; } diff --git a/web/app/assets/javascripts/download_jamtrack.js.coffee b/web/app/assets/javascripts/download_jamtrack.js.coffee index 8890e6b87..eb9846546 100644 --- a/web/app/assets/javascripts/download_jamtrack.js.coffee +++ b/web/app/assets/javascripts/download_jamtrack.js.coffee @@ -190,7 +190,7 @@ 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, context.JK.currentUserId, + context.jamClient.JamTrackDownload(@jamTrack.id, null, context.JK.currentUserId, this.makeDownloadProgressCallback(), this.makeDownloadSuccessCallback(), this.makeDownloadFailureCallback()) @@ -369,8 +369,8 @@ context.JK.DownloadJamTrack = class DownloadJamTrack @trackDetail = context.jamClient.JamTrackGetTrackDetail ("#{@jamTrack.id}-#{@sampleRateForFilename}") if @trackDetail.version? - @logger.error("after invalidating package, the version is still wrong!") - throw "after invalidating package, the version is still wrong!" + @logger.error("after invalidating package, the version is still wrong!", @trackDetail) + throw "after invalidating package, the version is still wrong! #{@trackDetail.version}" switch @trackDetail.key_state when 'pending' diff --git a/web/app/assets/javascripts/everywhere/everywhere.js b/web/app/assets/javascripts/everywhere/everywhere.js index f55ed52db..286214c4d 100644 --- a/web/app/assets/javascripts/everywhere/everywhere.js +++ b/web/app/assets/javascripts/everywhere/everywhere.js @@ -42,6 +42,9 @@ initializeInfluxDB(); trackNewUser(); + + trackScreenChanges(); + }) $(document).on('JAMKAZAM_READY', function() { @@ -188,6 +191,7 @@ context.stats.write = context.stats.writePoint; } + function initializeStun(app) { stun = new context.JK.Stun(app); context.JK.StunInstance = stun; @@ -209,9 +213,11 @@ JK.JamServer.registerMessageCallback(JK.MessageType.STOP_APPLICATION, function(header, payload) { context.jamClient.ShutdownApplication(); }); + } function handleGettingStarted(app) { + /** var user = app.user() if(user) { user.done(function(userProfile) { @@ -219,12 +225,13 @@ window.location.pathname.indexOf(gon.client_path) == 0 && window.location.hash.indexOf('/checkout') == -1 && window.location.hash.indexOf('/redeem') == -1 && + window.location.hash.indexOf('/teachers/setup') == -1 && !app.layout.isDialogShowing('getting-started')) { app.layout.showDialog('getting-started'); } }) - } + }*/ } function initShoppingCart(app) { @@ -253,6 +260,36 @@ } } + function trackScreenChanges() { + + if (!window.olark) { + return; + } + + var activate = ["jamtrack/search", "jamtrack/filter", + "shoppingCart", "checkoutPayment", "checkoutOrder", "redeemComplete", "checkoutComplete", + "teachers/setup/introduction", "teachers/setup/basics", "teachers/setup/experience", "teachers/setup/pricing", + "account/profile", "account/profile/experience", "account/profile/interests", "account/profile/samples", + "jamclass", "jamclass/searchOptions", "teachers/search", "profile/teacher", "jamclass/test-drive-selection", "jamclass/book-lesson"] + $(document).on(context.JK.EVENTS.SCREEN_CHANGED, function(e, data) { + + var show = false; + + if (data.newScreen && activate.indexOf(data.newScreen) > -1) { + show = true; + } + + if (show) { + olark('api.box.show'); + } + else { + olark('api.box.hide'); + } + + + }); + } + function setNavigationStart() { if(!window.performance || !window.performance.timing) { try { diff --git a/web/app/assets/javascripts/faderHelpers.js b/web/app/assets/javascripts/faderHelpers.js index 6d573a2c8..7cbab2f28 100644 --- a/web/app/assets/javascripts/faderHelpers.js +++ b/web/app/assets/javascripts/faderHelpers.js @@ -23,7 +23,14 @@ var $fader = $(this); var floaterConvert = $fader.data('floaterConverter') var sessionModel = window.JK.CurrentSessionModel || null; - + + /** + if(!$fader.data('has-session-control')) { + var sessionControllerName = $fader.data('session-controller-name'); + window.JK.prodBubble($fader, 'not-session-controller', {sessionControllerName:sessionControllerName}, {positions:['left', 'right'], offsetParent: $fader.closest('.top-parent'), duration:12000}) + return false; + }*/ + var mediaControlsDisabled = $fader.data('media-controls-disabled'); if(mediaControlsDisabled) { var mediaTrackOpener = $fader.data('media-track-opener'); @@ -173,7 +180,14 @@ var mediaControlsDisabled = $draggingFaderHandle.data('media-controls-disabled'); var mediaTrackOpener = $draggingFaderHandle.data('media-track-opener'); var sessionModel = window.JK.CurrentSessionModel || null; - + + /** + if(!$draggingFaderHandle.data('has-session-control')) { + var sessionControllerName = $draggingFaderHandle.data('session-controller-name'); + window.JK.prodBubble($draggingFaderHandle, 'not-session-controller', {sessionControllerName:sessionControllerName}, {positions:['left', 'right'], offsetParent: $draggingFaderHandle.closest('.top-parent'), duration:12000}) + return false; + }*/ + if(mediaControlsDisabled) { return false; } @@ -267,16 +281,33 @@ throw ("renderFader: userOptions is required"); } var renderDefaults = { - faderType: "vertical" + faderType: "vertical", + sessionController: null }; var options = $.extend({}, renderDefaults, userOptions); + var sessionCanControl = true + var sessionControllerName = null + if(userOptions.sessionController) { + if(!userOptions.sessionController.can_control) { + sessionCanControl = false + sessionControllerName = userOptions.sessionController.session_controller.name + } + } + selector.find('div[data-control="fader"]') .data('media-controls-disabled', selector.data('media-controls-disabled')) .data('media-track-opener', selector.data('media-track-opener')) .data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers')) .data('floaterConverter', floaterConverter) .data('snap', userOptions.snap) + .data('has-session-control', sessionCanControl) + .data('session-controller-name', sessionControllerName) + + + if(userOptions.sessionController) { + + } selector.find('div[data-control="fader-handle"]').draggable({ drag: onFaderDrag, @@ -289,6 +320,8 @@ .data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers')) .data('floaterConverter', floaterConverter) .data('snap', userOptions.snap) + .data('has-session-control', sessionCanControl) + .data('session-controller-name', sessionControllerName) // Embed any custom styles, applied to the .fader below selector if ("style" in options) { diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 464f992b8..7e2da352e 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -1,1299 +1,1791 @@ -(function(context,$) { +(function (context, $) { - "use strict"; + "use strict"; - context.JK = context.JK || {}; - context.JK.FakeJamClient = function(app, p2pMessageFactory) { - var ChannelGroupIds = context.JK.ChannelGroupIds - var logger = context.JK.logger; - logger.info("*** Fake JamClient instance initialized. ***"); + context.JK = context.JK || {}; + context.JK.FakeJamClient = function (app, p2pMessageFactory) { + var ChannelGroupIds = context.JK.ChannelGroupIds + var logger = context.JK.logger; + logger.info("*** Fake JamClient instance initialized. ***"); - // Change this to false if you want to see FTUE with fake jam client - var ftueStatus = true; - var eventCallbackName = ''; - var alertCallbackName = ''; - var eventCallbackRate = 1000; - var vuValue = -70; - var vuChange = 10; - var callbackTimer = null; - var _mix = -30; - var device_id = -1; - var latencyCallback = null; - 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 simulateNoInputs = false; + // Change this to false if you want to see FTUE with fake jam client + var ftueStatus = true; + var eventCallbackName = ''; + var alertCallbackName = ''; + var eventCallbackRate = 1000; + var vuValue = -70; + var vuChange = 10; + var callbackTimer = null; + var _mix = -30; + var device_id = -1; + var latencyCallback = null; + 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 simulateNoInputs = false; - function dbg(msg) { logger.debug('FakeJamClient: ' + msg); } + function dbg(msg) { + logger.debug('FakeJamClient: ' + msg); + } - // Bummer that javascript doesn't have much in the way of reflection. - // arguments.callee.name would probably do what we want, but it's deprecated - // and not allowed when using "strict" - // (Wishing I could write a single function which debug logs the name of the - // current function and a JSON stringify of the arguments). + // Bummer that javascript doesn't have much in the way of reflection. + // arguments.callee.name would probably do what we want, but it's deprecated + // and not allowed when using "strict" + // (Wishing I could write a single function which debug logs the name of the + // current function and a JSON stringify of the arguments). - function OpenSystemBrowser(href) { - dbg("OpenSystemBrowser('" + href + "')"); - context.window.open(href); - } - - function RestartApplication() {} - function ShutdownApplication() {} - function FTUEPageEnter() {} - function FTUEPageLeave() {} - function FTUECancel() {} - function FTUEGetMusicProfileName() { + function getClientParentChildRole() { + // 0 = client + // 1 = parent + return 1; + } - if(simulateNoInputs) { - return "System Default (Playback Only)" + function getParentClientId() { + return null; + } + + function OpenSystemBrowser(href) { + dbg("OpenSystemBrowser('" + href + "')"); + context.window.open(href); + } + + function RestartApplication() { + } + + function ShutdownApplication() { + } + + function FTUEPageEnter() { + } + + function FTUEPageLeave() { + } + + function FTUECancel() { + } + + function FTUEGetMusicProfileName() { + + 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) { + return true; + } + + function FTUESetVideoEncodeResolution(resolution) { + + } + + function testVideoRender() { + + } + + function FTUEGetVideoCaptureDeviceNames() { + return {"xy323ss": "Built-in Webcam HD"} + } + + function FTUECurrentSelectedVideoDevice() { + //return {} + return {"xy323ss": "Built-in Webcam HD"} + } + + function FTUEGetAvailableEncodeVideoResolutions() { + return { + 1: "CIF (352X288)", + 2: "VGA (640X480)", + 3: "4CIF (704X576)", + 4: "1/2WHD (640X360)", + 5: "WHD (1280X720)", + 6: "FHD (1920x1080)" + } + } + + function FTUEGetVideoCaptureDeviceCapabilities() { + return {} + } + + function FTUESetSendFrameRates(fps) { + + } + + function FTUEGetSendFrameRates() { + return {20: 20, 24: 24, 30: 30} + } + + function GetCurrentVideoResolution() { + return 3; + } + + function GetCurrentVideoFrameRate() { + return 30; + } + + function GetSampleRate() { + return 48; + } + + function FTUESetVideoShareEnable() { + + } + + function FTUEGetVideoShareEnable() { + return false; + } + + 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; + } + + function FTUESetInputLatency(latency) { + dbg("FTUESetInputLatency:" + latency); + } + + function FTUEGetOutputLatency() { + dbg("FTUEGetOutputLatency"); + return 2; + } + + function FTUESetOutputLatency(latency) { + dbg("FTUESetOutputLatency:" + latency); + } + + function FTUEGetVolumeRanges() { + dbg("FTUEGetVolumeRanges"); + return { + input_maximum: 20, + input_minimum: -80, + output_maximum: 20, + output_minimum: -80 + }; + } + + function FTUEHasControlPanel() { + dbg("FTUEHasControlPanel"); + return true; + } + + function FTUEGetFrameSize(newSize) { + dbg("FTUEGetFrameSize"); + return frameSize; + } + + function FTUESetFrameSize(newSize) { + dbg("FTUESetFrameSize:" + newSize); + // one of 2.5, 5 or 10 + frameSize = newSize; + } + + function FTUEOpenControlPanel() { + dbg("FTUEOpenControlPanel"); + context.alert("Simulated ASIO Dialog"); + } + + function FTUEInit() { + dbg("FTUEInit"); + } + + function FTUERefreshDevices() { + dbg("FTUERefreshDevices()"); + } + + function FTUESave(b) { + dbg("FTUESave(" + b + ")"); + return {}; + } + + function FTUEGetStatus() { + return ftueStatus; + } + + function FTUESetStatus(b) { + ftueStatus = b; + } + + function FTUESetMusicDevice(id) { + dbg("FTUESetMusicDevice"); + } + + function FTUEGetAudioDevices() { + return { + "devices": [{ + "display_name": "Built-in", + "guid": "Built-in", + "input_count": 1, + "name": "Built-in", + "output_count": 1, + "port_audio_name": "Built-in" + }, { + "display_name": "JamKazam Virtual Monitor", + "guid": "JamKazam Virtual Monitor", + "input_count": 0, + "name": "JamKazam Virtual Monitor", + "output_count": 1, + "port_audio_name": "JamKazam Virtual Monitor" + }] + } + } + + function FTUEGetDevices() { + dbg('FTUEGetMusicDevices'); + return { + "ASIO4ALL v2": "ASIO4ALL v2 - ASIO", + "M-Audio FW ASIO": "M-AUDIO FW ASIO - ASIO" + }; + } + + function FTUEGetMusicInputs() { + dbg('FTUEGetMusicInputs'); + return { + "i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~1": "Multichannel (FWAPMulti) - Channel 1/Multichannel (FWAPMulti) - Channel 2" + }; + } + + function FTUEGetMusicOutputs() { + dbg('FTUEGetMusicOutputs'); + return { + "o~11~Multichannel (FWAPMulti)~0^o~11~Multichannel (FWAPMulti)~1": "Multichannel (FWAPMulti) - Channel 1/Multichannel (FWAPMulti) - Channel 2" + }; + } + + function FTUEGetChatInputs() { + dbg('FTUEGetChatInputs'); + return { + "i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~1": "Multichannel (FWAPMulti) - Channel 1/Multichannel (FWAPMulti) - Channel 2" + }; + } + + function FTUEGetChannels() { + return { + "inputs": [ + { + "assignment": 1, + "chat": false, + "device_id": "Built-in Microph", + "device_type": 5, + "id": "i~5~Built-in Microph~0~0~Built-in", + "name": "Built-in Microph - Left", + "number": 0 + }, + { + "assignment": 0, + "chat": false, + "device_id": "Built-in Microph", + "device_type": 5, + "id": "i~5~Built-in Microph~1~0~Built-in", + "name": "Built-in Microph - Right", + "number": 1 } - else { - return "FTUEAttempt-1" + ], + "outputs": [ + { + "assignment": -1, + "chat": false, + "device_id": "Built-in Output", + "device_type": 5, + "id": "o~5~Built-in Output~0~0~Built-in", + "name": "Built-in Output - Left", + "number": 0 + }, + { + "assignment": -1, + "chat": false, + "device_id": "Built-in Output", + "device_type": 5, + "id": "o~5~Built-in Output~1~0~Built-in", + "name": "Built-in Output - Right", + "number": 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 {"xy323ss": "Built-in Webcam HD"} - } - function FTUECurrentSelectedVideoDevice() { - return {"xy323ss": "Built-in Webcam HD"} - //return {} - } - function FTUEGetAvailableEncodeVideoResolutions() { - return { - 1 : "CIF (352X288)", - 2 : "VGA (640X480)", - 3 : "4CIF (704X576)", - 4 : "1/2WHD (640X360)", - 5 : "WHD (1280X720)", - 6 : "FHD (1920x1080)" - } - } - - function FTUEGetVideoCaptureDeviceCapabilities() { - return {} - } - - function FTUESetSendFrameRates(fps) { - - } - - function FTUEGetSendFrameRates() { - return {20:20, 24:24, 30:30} - } - - function GetCurrentVideoResolution() { - return 3; - } - - function GetCurrentVideoFrameRate() { - return 30; - } - - 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; - } - function FTUESetInputLatency(latency) { - dbg("FTUESetInputLatency:" + latency); - } - function FTUEGetOutputLatency() { - dbg("FTUEGetOutputLatency"); - return 2; - } - function FTUESetOutputLatency(latency) { - dbg("FTUESetOutputLatency:" + latency); - } - function FTUEGetVolumeRanges () { - dbg("FTUEGetVolumeRanges"); - return { - input_maximum: 20, - input_minimum: -80, - output_maximum: 20, - output_minimum: -80 - }; - } - function FTUEHasControlPanel() { - dbg("FTUEHasControlPanel"); - return true; - } - function FTUEGetFrameSize(newSize) { - dbg("FTUEGetFrameSize"); - return frameSize; - } - function FTUESetFrameSize(newSize) { - dbg("FTUESetFrameSize:" + newSize); - // one of 2.5, 5 or 10 - frameSize = newSize; - } - function FTUEOpenControlPanel() { - dbg("FTUEOpenControlPanel"); - context.alert("Simulated ASIO Dialog"); - } - function FTUEInit() { dbg("FTUEInit"); } - function FTUERefreshDevices() { dbg("FTUERefreshDevices()"); } - function FTUESave(b) { dbg("FTUESave(" + b + ")"); return {}; } - function FTUEGetStatus() { return ftueStatus; } - function FTUESetStatus(b) { ftueStatus = b; } - function FTUESetMusicDevice(id) { dbg("FTUESetMusicDevice"); } - function FTUEGetAudioDevices() { - return {"devices":[{"display_name":"Built-in","guid":"Built-in","input_count":1,"name":"Built-in","output_count":1,"port_audio_name":"Built-in"},{"display_name":"JamKazam Virtual Monitor","guid":"JamKazam Virtual Monitor","input_count":0,"name":"JamKazam Virtual Monitor","output_count":1,"port_audio_name":"JamKazam Virtual Monitor"}]} - } - function FTUEGetDevices() { - dbg('FTUEGetMusicDevices'); - return { - "ASIO4ALL v2":"ASIO4ALL v2 - ASIO", - "M-Audio FW ASIO":"M-AUDIO FW ASIO - ASIO" - }; - } - function FTUEGetMusicInputs() { - dbg('FTUEGetMusicInputs'); - return { - "i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~1": - "Multichannel (FWAPMulti) - Channel 1/Multichannel (FWAPMulti) - Channel 2" - }; - } - function FTUEGetMusicOutputs() { - dbg('FTUEGetMusicOutputs'); - return { - "o~11~Multichannel (FWAPMulti)~0^o~11~Multichannel (FWAPMulti)~1": - "Multichannel (FWAPMulti) - Channel 1/Multichannel (FWAPMulti) - Channel 2" - }; - } - function FTUEGetChatInputs() { - dbg('FTUEGetChatInputs'); - return { - "i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~1": - "Multichannel (FWAPMulti) - Channel 1/Multichannel (FWAPMulti) - Channel 2" - }; - } - function FTUEGetChannels() { - return { - "inputs": [ - { - "assignment": 1, - "chat": false, - "device_id": "Built-in Microph", - "device_type": 5, - "id": "i~5~Built-in Microph~0~0~Built-in", - "name": "Built-in Microph - Left", - "number": 0 - }, - { - "assignment": 0, - "chat": false, - "device_id": "Built-in Microph", - "device_type": 5, - "id": "i~5~Built-in Microph~1~0~Built-in", - "name": "Built-in Microph - Right", - "number": 1 - } - ], - "outputs": [ - { - "assignment": -1, - "chat": false, - "device_id": "Built-in Output", - "device_type": 5, - "id": "o~5~Built-in Output~0~0~Built-in", - "name": "Built-in Output - Left", - "number": 0 - }, - { - "assignment": -1, - "chat": false, - "device_id": "Built-in Output", - "device_type": 5, - "id": "o~5~Built-in Output~1~0~Built-in", - "name": "Built-in Output - Right", - "number": 1 - } - ] - }; - } - - function FTUEClearChannelAssignments(){} - function FTUEClearChatInput(){} - function FTUEGetAudioDevices() { - return { - "devices": [ - { - "display_name": "Built-in", - "guid": "Built-in", - "input_count": 1, - "name": "Built-in", - "output_count": 1, - "port_audio_name": "Built-in" - }, - { - "display_name": "JamKazam Virtual Monitor", - "guid": "JamKazam Virtual Monitor", - "input_count": 0, - "name": "JamKazam Virtual Monitor", - "output_count": 1, - "port_audio_name": "JamKazam Virtual Monitor" - } - ] - }; - } - function FTUEStartIoPerfTest() {} - function FTUEGetIoPerfData() { - return { - "in_var" : 0.15, - "out_var" : 0.25, - "in_median" : 399.9, - "out_median" : 400.3, - "in_target" : 400, - "out_target" : 400 - }; - } - function FTUESetInputMusicDevice() { } - function FTUESetOutputMusicDevice() { } - function FTUEGetInputMusicDevice() { return 'Built-in'; } - function FTUEGetOutputMusicDevice() { return 'Built-in'; } - - function FTUESetMusicInput() { dbg('FTUESetMusicInput'); } - function FTUESetChatInput() { dbg('FTUESetChatInput'); } - function FTUESetMusicOutput() { dbg('FTUESetMusicOutput'); } - function FTUEGetInputVolume() { dbg('FTUEGetInputVolume'); return -60; } - function FTUESetInputVolume() { dbg('FTUESetInputVolume'); } - function FTUEGetOutputVolume() { dbg('FTUEGetOutputVolume'); return -40; } - function FTUESetOutputVolume() { dbg('FTUESetOutputVolume'); } - function FTUEGetChatInputVolume() { dbg('FTUEGetChatInputVolume'); return -10; } - function FTUESetChatInputVolume() { dbg('FTUESetChatInputVolume'); } - function FTUERegisterVUCallbacks() { - dbg('FTUERegisterVUCallbacks'); - } - function FTUERegisterLatencyCallback(functionName) { - dbg('FTUERegisterLatencyCallback'); - latencyCallback = functionName; - } - function FTUEStartLatency() { - function cb() { - // Change 4192 to modify latency MS results (in microseconds) - eval(latencyCallback + "(10.0)"); - } - context.setTimeout(cb, 1000); - } - - function FTUEGetExpectedLatency() { - return {latencyknown:true, latency:5} - } - - function FTUEGetGoodConfigurationList() { - return ['default']; - } - - function FTUEGetAllAudioConfigurations() { - return ['default']; - } - - function FTUEGetGoodAudioConfigurations() { - return ['default']; - } - - function FTUEGetConfigurationDevice() { - return 'Good Device'; - } - - function FTUELoadAudioConfiguration() { - return true; - } - - function FTUEIsMusicDeviceWDM() { - return false; - } - - function FTUECreateUpdatePlayBackProfile() { - return true; - } - - function RegisterVolChangeCallBack(functionName) { - dbg('RegisterVolChangeCallBack'); - } - - - function GetOS() { return 100000000; } - function GetOSAsString() { - return "Win32"; - //return "MacOSX"; - } - - function LatencyUpdated(map) { dbg('LatencyUpdated:' + JSON.stringify(map)); } - function LeaveSession(map) { dbg('LeaveSession:' + JSON.stringify(map)); } - - // this is not a real bridge method; purely used by the fake jam client - function RegisterP2PMessageCallbacks(callbacks) { - p2pCallbacks = callbacks; - } - - function P2PMessageReceived(from, payload) { - - dbg('P2PMessageReceived'); - // this function is different in that the payload is a JSON ready string; - // whereas a real p2p message is base64 encoded binary packaged data - - try { - payload = JSON.parse(payload); - } - catch(e) { - logger.warn("unable to parse payload as JSON from client %o, %o, %o", from, e, payload); - } - - var callback = p2pCallbacks[payload.type]; - if(callback) { - callback(from, payload); - } - } - - function JoinSession(sessionId) {dbg('JoinSession:' + sessionId);} - function ParticipantLeft(session, participant) { - dbg('ParticipantLeft:' + JSON.stringify(session) + ',' + - JSON.stringify(participant)); - } - function ParticipantJoined(session, participant) { - dbg('ParticipantJoined:' + JSON.stringify(session) + ',' + - JSON.stringify(participant)); - } - function RecordTestBegin() { dbg('RecordTestBegin'); } - function RecordTestEnd() { dbg('RecordTestBegin'); } - function RecordTestPlayback() { dbg('RecordTestBegin'); } - function SendP2PMessage(s1,s2) { dbg('SendP2PMessage:' + s1 + ',' + s2); } - function SetASIOEnabled(i1, i2, i3, i4, i5, i6) { - dbg('SetASIOEnabled(' + i1 + ',' + i2 + ',' + i3 + ',' + i4 + ',' + i5 + ',' + i6 + ')'); - } - function SignalLatencyUpdated(client) { - dbg('SignalLatencyUpdated:' + JSON.stringify(client)); - } - function SignalSendP2PMessage(s1,ba) { - dbg('SignalSendP2PMessage:' + JSON.stringify(arguments)); - } - function StartPlayTest(s) { dbg('StartPlayTest' + JSON.stringify(arguments)); } - function StartRecordTest(s) { dbg('StartRecordTest' + JSON.stringify(arguments)); } - function StartRecording(recordingId, groupedClientTracks) { - dbg('StartRecording'); - fakeJamClientRecordings.StartRecording(recordingId, groupedClientTracks); - } - function StopPlayTest() { dbg('StopPlayTest'); } - function StopRecording(recordingId, groupedTracks, errorReason, detail) { - dbg('StopRecording'); - fakeJamClientRecordings.StopRecording(recordingId, groupedTracks, errorReason, detail); - } - - function AbortRecording(recordingId, errorReason, errorDetail) { - dbg('AbortRecording'); - fakeJamClientRecordings.AbortRecording(recordingId, errorReason, errorDetail); - } - function OnTrySyncCommand(cmd) { - - } - function GetRecordingManagerState() { - return {running: false} - } - - function TestASIOLatency(s) { dbg('TestASIOLatency' + JSON.stringify(arguments)); } - - function TestLatency(clientID, callbackFunctionName, timeoutCallbackName) { - logger.debug("Fake JamClient: TestLatency called with client, " + clientID + " and callback function name: " + callbackFunctionName); - var response = { - clientID: clientID, - latency: 50 - }; - var js = callbackFunctionName + "(" + JSON.stringify(response) + ");"; - eval(js); - } - - function IsMyNetworkWireless() { - // 1=true, 0 = false, -1=unknown - return 1; - } - - function SetNetworkTestScore(numClients) { - - } - - function SetVideoNetworkTestScore(numClients) { - - } - - function GetNetworkTestScore() { - return 8; - } - - - function GetVideoNetworkTestScore() { - return 8; - } - - function SetLatencyTestBlocked(blocked) { - - } - - function isLatencyTestBlocked() { - return false; - } - - function GetLastLatencyTestTimes() { - return { initiated: 10000, requested: 10000} - } - - function GetASIODevices() { - var response =[{"device_id":0,"device_name":"Realtek High Definition Audio","device_type": 0,"interfaces":[{"interface_id":0,"interface_name":"Realtek HDA SPDIF Out","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":1,"interface_name":"Realtek HD Audio rear output","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":2,"interface_name":"Realtek HD Audio Mic input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":3,"interface_name":"Realtek HD Audio Line input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":4,"interface_name":"Realtek HD Digital input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"}]},{"interface_id":5,"interface_name":"Realtek HD Audio Stereo input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]}],"wavert_supported":false},{"device_id":1,"device_name":"M-Audio FW Audiophile","device_type": 1,"interfaces":[{"interface_id":0,"interface_name":"FWAPMulti","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":1,"interface_name":"FW AP 1/2","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":2,"interface_name":"FW AP SPDIF","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":3,"interface_name":"FW AP 3/4","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":2,"device_name":"Virtual Audio Cable","device_type": 2,"interfaces":[{"interface_id":0,"interface_name":"Virtual Cable 2","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]},{"interface_id":1,"interface_name":"Virtual Cable 1","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":3,"device_name":"WebCamDV WDM Audio Capture","device_type": 3,"interfaces":[{"interface_id":0,"interface_name":"WebCamDV Audio","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"},{"is_input":false,"pin_id":1,"pin_name":"Volume Control"}]}],"wavert_supported":false}]; - return response; - } - - // Session Functions - - function SessionCurrrentJamTrackPlayPosMs() { - return 0; - } - - function SessionGetJamTracksPlayDurationMs() { - return 60000; - } - function SessionAddTrack() {} - - function SessionAudioResync() { - logger.debug("Fake JamClient: SessionAudioResync()"); - } - - function SessionGetAllControlState(isMasterOrPersonal) { - var mixerIds = SessionGetIDs() - return SessionGetControlState(mixerIds, isMasterOrPersonal); - } - function SessionGetControlState(mixerIds, isMasterOrPersonal) { - dbg("SessionGetControlState"); - var groups = - [ChannelGroupIds.MasterGroup, - ChannelGroupIds.MonitorGroup, - ChannelGroupIds.AudioInputMusicGroup, - ChannelGroupIds.AudioInputChatGroup, - ChannelGroupIds.AudioInputChatGroup, - ChannelGroupIds.UserMusicInputGroup, - ChannelGroupIds.UserChatInputGroup, - ChannelGroupIds.PeerMediaTrackGroup, - ChannelGroupIds.JamTrackGroup, - ChannelGroupIds.MetronomeGroup]; - var names = [ - "FWAPMulti", - "FWAPMulti", - "FWAPMulti", - "FWAPMulti", - "", - "", - "", - "", - "", - "" - ]; - - var media_types = [ - "Master", - "Monitor", - "AudioInputMusic", - "AudioInputChat", - "StreamOutMusic", - "UserMusicInput", - "PeerAudioInputMusic", - "PeerMediaTrack", - "JamTrack", - "MetronomeTrack" - ] - var clientIds = [ - "", - "", - "", - "", - "3933ebec-913b-43ab-a4d3-f21dc5f8955b", - "", - "", - "", - "", - "" - ]; - var response = []; - for (var i=0; i 10 || vuValue < -70) { + vuChange = vuChange * -1; + } + + } + + function SetVURefreshRate(rateMS) { + eventCallbackRate = rateMS; + if (callbackTimer) { + context.clearInterval(callbackTimer); + } + if (eventCallbackName) { + callbackTimer = context.setInterval(doCallbacks, eventCallbackRate); + } + } + + // Track Functions + + + // Returns a list of objects representing all available audio devices and + // pins on the current system. On my windows box, I get 38 objects back. + // First couple examples included here. Note that the list tends to come + // back with all of the left inputs, then all the right inputs, then all + // the left outputs, then all the right outputs. + function TrackGetChannels() { + // Real example: + /* + { + device_id: "1394\\M-Audio&FW_Audiophile", + full_id: "i~1~\\\\?\\1394#m-audio&fw_audiophile#d4eb0700036c0d00#{6994ad04-93ef-11d0-a3cc-00a0c9223196}\\fwap_12", + id: "\\\\?\\1394#m-audio&fw_audiophile#d4eb0700036c0d00#{6994ad04-93ef-11d0-a3cc-00a0c9223196}\\fwap_12", + input: true, + left: true, + name: "M-Audio FW Audiophile: FW AP 1/2 - Left" + }, + */ + + // But we'll just build a list of names and fake it + var devices = [ + "M-Audio FW Audiophile: FW AP 1/2" + //"M-Audio FW Audiophile: FW Multi 1/2", + //"M-Audio FW Audiophile: FW SPDIF 1/2", + //"Realtek High Definition Audio: Realtek HD Digital", + // "Realtek High Definition Audio: Realtek HD Line", + // "Realtek High Definition Audio: Realtek HD Audio Mic", + // "Realtek High Definition Audio: Realtek HD Audio Stereo", + //"WebCamDV WDM Audio Capture: WebCamDV Audio" + ]; + var suffixes = [" - Left", " - Right", " - Left", " - Right"]; + var lefts = [true, false, true, false]; + var inputs = [true, true, false, false]; + var types = ["music", "non-music", "music", "non-music"]; + var response = []; + var name, o, i, j; + for (j = 0; j < 4; j++) { + for (i = 0; i < devices.length; i++) { + name = devices[i] + suffixes[j]; + o = { + device_id: name, + full_id: name, + id: name, + input: inputs[j], + left: lefts[j], + name: name, + device_type: types[j] + }; + response.push(o); } + } + return response; + } - function SessionSetControlState(stringValue, isMasterOrPersonal) { - dbg('SessionSetControlState: ' + stringValue + " m/p=" + isMasterOrPersonal); - } - function SessionSetRecordingFilename(filename) {} - function SessionSetRecordingFolder(folderName) {} - function SessionStartPlay() {} - function SessionStartRecording() {} - function SessionStopPlay() {} - function SessionStopRecording() {} - function SessionAddPlayTrack() {return true;} - function SessionRemoveAllPlayTracks(){} - function isSessionTrackPlaying() { return false; } - function SessionCurrrentPlayPosMs() { return 0; } - function SessionGetTracksPlayDurationMs() { return 0; } - function SessionGetDeviceLatency() { return 10.0; } - function SessionPageEnter() {logger.debug("FakeJamClient: SessionPageEnter"); return {}} - function SessionPageLeave() {logger.debug("FakeJamClient: SessionPageLeave")} - function SetMixerMode(mode) {} - function SessionGetMasterLocalMix() { - logger.debug('SessionGetMasterLocalMix. Returning: ' + _mix); - return _mix; - } - function SessionSetMasterLocalMix(level) { - logger.debug('SessionSetMasterLocalMix(' + level + ')'); - _mix = level; - } - function SessionSetUserName(client_id,name){ + function TrackGetCount() { + return 1; // Real version returns whatever was set + } - } + function TrackGetInstrument(trackNumber) { + if (trackNumber === 1) { + return 100; + } + else { + return 200; + } + } - function doCallbacks() { - var names = ["vu"]; - //var ids = ["FWAPMulti_2_10200", "FWAPMulti_0_10000"]; - var ids= ["i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~1", - "i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~2"]; + function TrackGetChatEnable() { + return true; + } - var args = []; - for (var i=0; i 10 || vuValue < -70) { vuChange = vuChange * -1; } + } - } - function SetVURefreshRate(rateMS) { - eventCallbackRate = rateMS; - if (callbackTimer) { context.clearInterval(callbackTimer); } - if (eventCallbackName) { - callbackTimer = context.setInterval(doCallbacks, eventCallbackRate); - } - } + function TrackSaveAssignments() { + } - // Track Functions + function TrackGetDevices() { + var response = { + "Built-in": "Built-in", + "ASIO4ALL v2": "ASIO4ALL v2 - ASIO", + "BEHRINGER USB AUDIO": "BEHRINGER USB AUDIO - ASIO", + "{2B7D1F21-FA6E-11D4-0002-DDCA793267F2}": "SoundMAX HD Audio O - WDM", + "{ABCC5A6C-C263-463B-A72F-A5BF64C86EBA}": "Generic USB Audio Device - WDM" + }; + return response; + } + + function TrackDeleteProfile(id) { + + } - // Returns a list of objects representing all available audio devices and - // pins on the current system. On my windows box, I get 38 objects back. - // First couple examples included here. Note that the list tends to come - // back with all of the left inputs, then all the right inputs, then all - // the left outputs, then all the right outputs. - function TrackGetChannels() { - // Real example: - /* -{ - device_id: "1394\\M-Audio&FW_Audiophile", - full_id: "i~1~\\\\?\\1394#m-audio&fw_audiophile#d4eb0700036c0d00#{6994ad04-93ef-11d0-a3cc-00a0c9223196}\\fwap_12", - id: "\\\\?\\1394#m-audio&fw_audiophile#d4eb0700036c0d00#{6994ad04-93ef-11d0-a3cc-00a0c9223196}\\fwap_12", - input: true, - left: true, - name: "M-Audio FW Audiophile: FW AP 1/2 - Left" -}, - */ + function TrackGetMusicDeviceID() { + return device_id; + } - // But we'll just build a list of names and fake it - var devices = [ - "M-Audio FW Audiophile: FW AP 1/2" - //"M-Audio FW Audiophile: FW Multi 1/2", - //"M-Audio FW Audiophile: FW SPDIF 1/2", - //"Realtek High Definition Audio: Realtek HD Digital", - // "Realtek High Definition Audio: Realtek HD Line", - // "Realtek High Definition Audio: Realtek HD Audio Mic", - // "Realtek High Definition Audio: Realtek HD Audio Stereo", - //"WebCamDV WDM Audio Capture: WebCamDV Audio" - ]; - var suffixes = [ " - Left", " - Right", " - Left", " - Right"]; - var lefts = [true, false, true, false]; - var inputs = [true, true, false, false]; - var types = ["music", "non-music", "music", "non-music"]; - var response = []; - var name, o, i, j; - for (j=0; j<4; j++) { - for (i=0; i 0.5 + } + } + + function getPeerState(clientId) { + return { + ntp_stable: Math.random() > 0.5 + } + } - function TrackGetMusicDeviceID() { - return device_id; - } + // stun + function NetworkTestResult() { + return {remote_udp_blocked: false} + } - function TrackSetMusicDevice(id) { - device_id = id; - } + // Client Update Functions + function IsAppInWritableVolume() { + return true; + } - function TrackGetMusicDeviceNames(input) { - var names = []; - names.push("Built-in"); - return names; - } + function ClientUpdateVersion() { + return "Compiled 1.2.3"; + } - // mock response of this method by allowing only even device IDs to open control panel - function TrackHasControlPanel() { - return true; - } + function ClientUpdateStartDownload(url, progressCallback, successCallback, failureCallback) { - function TrackIsMusicDeviceType(type) { - return true; - } + // simulate a bunch of download callbacks + var count = 0; + var max = 100; + var bytesReceived = 0; + var bytesTotal = 10000; - function TrackGetChatUsesMusic() { - return 0; - } + function fire() { + count++; + context.setTimeout(function () { + bytesReceived = ( count / max ) * bytesTotal; + context.JK.ClientUpdate.DownloadProgressCallback(bytesReceived, bytesTotal, 0, 0); - function TrackSetChatUsesMusic(usesMusic) { - } - - function TrackOpenControlPanel() { - return; - } - - function TrackLoadAssignments() {} - - function TrackSave() {} - - // Set a Track Assignment. - // assignment is an enum: - // CHAT = -2, OUTPUT = -1, UNASSIGNED = 0 - function TrackSetAssignment(chanId, isInput, assignment) {} - - // Get a Track Assignment. - function TrackGetAssignment(chanId, isInput) { - // CHAT = -2, OUTPUT = -1, UNASSIGNED = 0 - return 0; - } - - function TrackSetCount(count) {} - - function TrackSetInstrument(track, instrumentId) {} - - function JamTrackStopPlay() {} - function JamTrackPlay(){return true; } - function JamTrackIsPlayable() { - return true; - } - function JamTrackGetTrackDetail() { - return {key_state: 'unknown'} - } - function JamTrackKeysRequest() {} - function JamTrackDownload() {} - // Method which sets volume - function UpdateMixer(mixerId) {} - - // scoring knobs - function GetScoreWorkTimingInterval() { return {interval: 1000, backoff:60000} } - function SetScoreWorkTimingInterval(knobs) {return true;} - - function SessionOpenBackingTrackFile(path, loop) { - backingTrackPath = path - backingTrackLoop = loop - } - - function SessionSetBackingTrackFileLoop(path, loop) { - backingTrackPath = path - backingTrackLoop = loop - } - - function SessionCloseBackingTrackFile(path) { - backingTrackPath="" - } - - function SessionOpenMetronome(bpm, click, meter, mode){ - logger.debug("Setting metronome BPM: ", bpm) - metronomeActive =true - metronomeBPM = bpm - metronomeSound = click - metronomeMeter = meter - } - - //change setting - click. Mode 0: = mono, 1, = left ear, 2= right ear - function SessionSetMetronome(bpm,click,meter, mode){ - SessionOpenMetronome(bpm, click, meter, mode) - } - - //close everywhere - function SessionCloseMetronome(){ - metronomeActive=false - } - - function setMetronomeOpenCallback(callback) { - - } - - function getMyNetworkState() { - return { - ntp_stable: Math.random() > 0.5 - } - } - - function getPeerState(clientId) { - return { - ntp_stable: Math.random() > 0.5 - } - } - - - // stun - function NetworkTestResult() { return {remote_udp_blocked: false} } - - // Client Update Functions - function IsAppInWritableVolume() { return true; } - function ClientUpdateVersion() { return "Compiled 1.2.3"; } - function ClientUpdateStartDownload(url, progressCallback, successCallback, failureCallback) { - - // simulate a bunch of download callbacks - var count = 0; - var max = 100; - var bytesReceived = 0; - var bytesTotal = 10000; - - function fire() { - count++; - context.setTimeout(function() { - bytesReceived = ( count / max ) * bytesTotal; - context.JK.ClientUpdate.DownloadProgressCallback(bytesReceived, bytesTotal, 0, 0); - - if(count < max) { - fire(); - } - else { - context.JK.ClientUpdate.DownloadFailureCallback("/some/path/here"); - } - }, 50); - } + if (count < max) { fire(); - - } - - function getBackingTrackList() { - return {backing_tracks: [ - {name:"This is a really long name for a song dude.mp3", size:4283}, - {name:"foo.mp3",size:325783838} - ]}; - } - - function ClientUpdateStartUpdate(path, successCallback, failureCallback) {} - - // ------------------------------- - // fake jam client methods - // not a part of the actual bridge - // ------------------------------- - function SetFakeRecordingImpl(fakeRecordingsImpl) { - fakeJamClientRecordings = fakeRecordingsImpl; - } - - function TestNetworkPktBwRate(targetClientId, successCallback, timeoutCallback, testType, duration, numClients, payloadSize) { - var progress = {progress:true, - upthroughput:.95, - downthroughput:.95, - upjitter: 2.3, - downjitter: 2.3 - } - var count = 0; - var interval = setInterval(function() { - - eval(successCallback + "(" + JSON.stringify(progress) + ");"); - - if(progress.upthroughput < 1) { - progress.upthroughput += .05; - } - if(progress.downthroughput < 1) { - progress.downthroughput += .05; - } - - count++; - if(count == duration) { - clearInterval(interval); - - delete progress['progress'] - progress.pass = true; - eval(successCallback + "(" + JSON.stringify(progress) + ");"); - } - }, 1000); - } - function StopNetworkTest(targetClientId) {} - function OnLoggedIn(userId, sessionToken) {} - function OnLoggedOut() {} - function UserAttention(option) {} - function IsFrontendVisible() {return true;} - function LastUsedProfileName() {return 'default'}; - function SetLastUsedProfileName(name) {return true;}; - function log(level, message) { - console.log("beep : " + message) - } - - function getOperatingMode() { - if (location.pathname == '/latency_tester') { - return 'server'; } else { - return 'client'; + context.JK.ClientUpdate.DownloadFailureCallback("/some/path/here"); } + }, 50); + } + + fire(); + + } + + function getBackingTrackList() { + return { + backing_tracks: [ + {name: "This is a really long name for a song dude.mp3", size: 4283}, + {name: "foo.mp3", size: 325783838} + ] + }; + } + + function ClientUpdateStartUpdate(path, successCallback, failureCallback) { + } + + // ------------------------------- + // fake jam client methods + // not a part of the actual bridge + // ------------------------------- + function SetFakeRecordingImpl(fakeRecordingsImpl) { + fakeJamClientRecordings = fakeRecordingsImpl; + } + + function TestNetworkPktBwRate(targetClientId, successCallback, timeoutCallback, testType, duration, numClients, payloadSize) { + var progress = { + progress: true, + upthroughput: .95, + downthroughput: .95, + upjitter: 2.3, + downjitter: 2.3 + } + var count = 0; + var interval = setInterval(function () { + + eval(successCallback + "(" + JSON.stringify(progress) + ");"); + + if (progress.upthroughput < 1) { + progress.upthroughput += .05; + } + if (progress.downthroughput < 1) { + progress.downthroughput += .05; } - // passed an array of recording objects from the server - function GetLocalRecordingState(recordings) { - var result = { recordings:[]}; - var recordingResults = result.recordings; + count++; + if (count == duration) { + clearInterval(interval); - var possibleAnswers = ['HQ', 'RT', 'MISSING', 'PARTIALLY_MISSING']; - - $.each(recordings, function(i, recording) { - // just make up a random yes-hq/yes-rt/missing answer - var recordingResult = {}; - recordingResult['aggregate_state'] = possibleAnswers[Math.floor((Math.random()*4))]; - recordingResults.push(recordingResult); - }) - - return result; + delete progress['progress'] + progress.pass = true; + eval(successCallback + "(" + JSON.stringify(progress) + ");"); } + }, 1000); + } - function OpenRecording(claimedRecording) { - return {success: true} - } - function PreviewRecording(claimedRecording) { - return OpenRecording(claimedRecording); - } - function CloseRecording() {} - function ClosePreviewRecording() {CloseRecording();} - function OnDownloadAvailable() {} - function SaveToClipboard(text) {} - function IsNativeClient() { /* must always return false in all scenarios due to not ruin scoring !*/ return false; } - function IsAudioStarted() { logger.debug("FakeJamClient: IsAudioStarted"); return false; } - function StopAudio() {logger.debug("FakeJamClient: StopAudio"); } - function ResetPageCounters() {logger.debug("FakeJamClient: ResetPageCounters"); } - function ReloadAudioSystem(leaveRunning, loadLastProfile, reinitAssignments) {logger.debug("FakeJamClient: ReloadAudioSystem"); return {};} - function SessionLiveBroadcastStart(host, port, mount, sourceUser, sourcePass, preferredClientId, bitrate) - { - logger.debug("SessionLiveBroadcastStart requested"); - } + function StopNetworkTest(targetClientId) { + } - function SessionLiveBroadcastStop() { - logger.debug("SessionLiveBroadcastStop requested"); - } + function OnLoggedIn(userId, sessionToken) { + } - function RegisterQuitCallback() { } - function LeaveSessionAndMinimize() {} - function SetAutoStart() {} - function GetAutoStart() { return true; } - function SaveSettings() {} + function OnLoggedOut() { + } - // Javascript Bridge seems to camel-case - // Set the instance functions: - this.AbortRecording = AbortRecording; - this.OnTrySyncCommand = OnTrySyncCommand; - this.GetRecordingManagerState = GetRecordingManagerState; - this.GetASIODevices = GetASIODevices; - this.GetOS = GetOS; - this.GetOSAsString = GetOSAsString; - this.JoinSession = JoinSession; - this.LatencyUpdated = LatencyUpdated; - this.LeaveSession = LeaveSession; - this.P2PMessageReceived = P2PMessageReceived; - this.ParticipantJoined = ParticipantJoined; - this.ParticipantLeft = ParticipantLeft; - this.RecordTestBegin = RecordTestBegin; - this.RecordTestEnd = RecordTestEnd; - this.RecordTestPlayback = RecordTestPlayback; - this.RegisterVolChangeCallBack = RegisterVolChangeCallBack; - this.SendP2PMessage = SendP2PMessage; - this.SetASIOEnabled = SetASIOEnabled; - this.SignalLatencyUpdated = SignalLatencyUpdated; - this.SignalSendP2PMessage = SignalSendP2PMessage; - this.StartPlayTest = StartPlayTest; - this.StartRecordTest = StartRecordTest; - this.StartRecording = StartRecording; - this.StopPlayTest = StopPlayTest; - this.StopRecording = StopRecording; - this.TestASIOLatency = TestASIOLatency; - this.TestLatency = TestLatency; - this.IsMyNetworkWireless = IsMyNetworkWireless; - this.SetNetworkTestScore = SetNetworkTestScore; - this.GetNetworkTestScore = GetNetworkTestScore; - this.SetVideoNetworkTestScore = SetVideoNetworkTestScore; - this.GetVideoNetworkTestScore = GetVideoNetworkTestScore; - this.SetLatencyTestBlocked = SetLatencyTestBlocked; - this.isLatencyTestBlocked = isLatencyTestBlocked; - this.GetLastLatencyTestTimes = GetLastLatencyTestTimes; - this.RegisterQuitCallback = RegisterQuitCallback; - this.LeaveSessionAndMinimize = LeaveSessionAndMinimize; - this.GetAutoStart = GetAutoStart; - this.SetAutoStart = SetAutoStart; - this.connected = true; + function UserAttention(option) { + } - // FTUE (round 3) - this.FTUESetInputMusicDevice = FTUESetInputMusicDevice; - this.FTUESetOutputMusicDevice = FTUESetOutputMusicDevice; - this.FTUEGetInputMusicDevice = FTUEGetInputMusicDevice; - this.FTUEGetOutputMusicDevice = FTUEGetOutputMusicDevice; - this.FTUEGetChatInputVolume = FTUEGetChatInputVolume; - this.FTUEGetChatInputs = FTUEGetChatInputs; - this.FTUEGetChannels = FTUEGetChannels; - this.FTUEGetAudioDevices = FTUEGetAudioDevices; - this.FTUEStartIoPerfTest = FTUEStartIoPerfTest; - this.FTUEGetIoPerfData = FTUEGetIoPerfData; - this.FTUEGetDevices = FTUEGetDevices; - this.FTUEGetFrameSize = FTUEGetFrameSize; - this.FTUECancel = FTUECancel; - this.FTUEPageEnter = FTUEPageEnter; - this.FTUEPageLeave = FTUEPageLeave; - this.FTUEGetMusicProfileName = FTUEGetMusicProfileName; - this.FTUESetMusicProfileName = FTUESetMusicProfileName; - this.FTUESetPreferredMixerSampleRate = FTUESetPreferredMixerSampleRate; - this.FTUESetPreferredOutputSampleRate = FTUESetPreferredOutputSampleRate; - this.FTUESetPreferredChatSampleRate = FTUESetPreferredChatSampleRate; - this.FTUEgetInputDeviceSampleRate = FTUEgetInputDeviceSampleRate; - this.FTUEgetOutputDeviceSampleRate = FTUEgetOutputDeviceSampleRate; - this.FTUEGetInputLatency = FTUEGetInputLatency; - this.FTUEGetInputVolume = FTUEGetInputVolume; - this.FTUEGetMusicInputs = FTUEGetMusicInputs; - this.FTUEGetMusicOutputs = FTUEGetMusicOutputs; - this.FTUEGetOutputLatency = FTUEGetOutputLatency; - this.FTUEGetOutputVolume = FTUEGetOutputVolume; - this.FTUEGetStatus = FTUEGetStatus; - this.FTUEGetVolumeRanges = FTUEGetVolumeRanges; - this.FTUEHasControlPanel = FTUEHasControlPanel; - this.FTUEInit = FTUEInit; - this.FTUEOpenControlPanel = FTUEOpenControlPanel; - this.FTUERegisterLatencyCallback = FTUERegisterLatencyCallback; - this.FTUERegisterVUCallbacks = FTUERegisterVUCallbacks; - this.FTUERefreshDevices = FTUERefreshDevices; - this.FTUESave = FTUESave; - this.FTUESetChatInput = FTUESetChatInput; - this.FTUESetChatInputVolume = FTUESetChatInputVolume; - this.FTUESetInputVolume = FTUESetInputVolume; - this.FTUESetFrameSize = FTUESetFrameSize; - this.FTUESetInputLatency = FTUESetInputLatency; - this.FTUESetMusicDevice = FTUESetMusicDevice; - this.FTUESetMusicInput = FTUESetMusicInput; - this.FTUESetMusicOutput = FTUESetMusicOutput; - this.FTUESetOutputLatency = FTUESetOutputLatency; - this.FTUESetOutputVolume = FTUESetOutputVolume; - this.FTUESetStatus = FTUESetStatus; - this.FTUEStartLatency = FTUEStartLatency; - this.FTUEGetExpectedLatency = FTUEGetExpectedLatency; - this.FTUEGetGoodConfigurationList = FTUEGetGoodConfigurationList; - this.FTUEGetAllAudioConfigurations = FTUEGetAllAudioConfigurations; - this.FTUEGetGoodAudioConfigurations = FTUEGetGoodAudioConfigurations; - this.FTUEGetConfigurationDevice = FTUEGetConfigurationDevice; - this.FTUEIsMusicDeviceWDM = FTUEIsMusicDeviceWDM; - this.FTUELoadAudioConfiguration = FTUELoadAudioConfiguration; - this.FTUEClearChannelAssignments = FTUEClearChannelAssignments; - this.FTUEClearChatInput = FTUEClearChatInput; - this.FTUECreateUpdatePlayBackProfile = FTUECreateUpdatePlayBackProfile; + function IsFrontendVisible() { + return true; + } - // Session - this.SessionAddTrack = SessionAddTrack; - this.SessionCurrrentJamTrackPlayPosMs = SessionCurrrentJamTrackPlayPosMs; - this.SessionGetJamTracksPlayDurationMs = SessionGetJamTracksPlayDurationMs; - this.SessionGetControlState = SessionGetControlState; - this.SessionGetAllControlState = SessionGetAllControlState; - this.SessionSetUserName = SessionSetUserName; - this.SessionGetIDs = SessionGetIDs; - this.RegisterRecordingManagerCallbacks = RegisterRecordingManagerCallbacks; - this.RegisterRecordingCallbacks = RegisterRecordingCallbacks; - this.SessionRegisterCallback = SessionRegisterCallback; - this.SessionSetAlertCallback = SessionSetAlertCallback; - this.SessionSetControlState = SessionSetControlState; - this.SessionSetRecordingFilename = SessionSetRecordingFilename; - this.SessionSetRecordingFolder = SessionSetRecordingFolder; - this.SessionStartPlay = SessionStartPlay; - this.SessionStartRecording = SessionStartRecording; - this.SessionStopPlay = SessionStopPlay; - this.SessionAddPlayTrack = SessionAddPlayTrack; - this.SessionRemoveAllPlayTracks = SessionRemoveAllPlayTracks; - this.SessionStopRecording = SessionStopRecording; - this.isSessionTrackPlaying = isSessionTrackPlaying; - this.SessionCurrrentPlayPosMs = SessionCurrrentPlayPosMs; - this.SessionGetTracksPlayDurationMs = SessionGetTracksPlayDurationMs; - this.SessionPageEnter = SessionPageEnter; - this.SessionPageLeave = SessionPageLeave; - this.SetMixerMode = SetMixerMode; - - this.SetVURefreshRate = SetVURefreshRate; - this.SessionGetMasterLocalMix = SessionGetMasterLocalMix; - this.SessionSetMasterLocalMix = SessionSetMasterLocalMix; - this.SessionGetDeviceLatency = SessionGetDeviceLatency; - this.SessionAudioResync = SessionAudioResync; - - // Track - this.TrackGetChannels = TrackGetChannels; - this.TrackSetAssignment = TrackSetAssignment; - this.TrackGetAssignment = TrackGetAssignment; - this.TrackSetCount = TrackSetCount; - this.TrackGetCount = TrackGetCount; - this.TrackSave = TrackSave; - this.TrackLoadAssignments = TrackLoadAssignments; - this.TrackSetInstrument = TrackSetInstrument; - this.TrackGetInstrument = TrackGetInstrument; - this.TrackSaveAssignments = TrackSaveAssignments; - - this.TrackGetDevices = TrackGetDevices; - this.TrackDeleteProfile = TrackDeleteProfile; - this.TrackGetMusicDeviceID = TrackGetMusicDeviceID; - this.TrackSetMusicDevice = TrackSetMusicDevice; - this.TrackGetMusicDeviceNames = TrackGetMusicDeviceNames; - this.TrackHasControlPanel = TrackHasControlPanel; - this.TrackOpenControlPanel = TrackOpenControlPanel; - this.TrackIsMusicDeviceType = TrackIsMusicDeviceType; - - this.TrackGetChatEnable = TrackGetChatEnable; - this.TrackSetChatEnable = TrackSetChatEnable; - - this.TrackGetChatUsesMusic = TrackGetChatUsesMusic; - this.TrackSetChatUsesMusic = TrackSetChatUsesMusic; - - this.JamTrackStopPlay = JamTrackStopPlay; - this.JamTrackPlay = JamTrackPlay; - this.JamTrackIsPlayable = JamTrackIsPlayable; - this.JamTrackGetTrackDetail = JamTrackGetTrackDetail; - this.JamTrackKeysRequest = JamTrackKeysRequest; - this.JamTrackDownload = JamTrackDownload; - - // Scoring Knobs - this.GetScoreWorkTimingInterval = GetScoreWorkTimingInterval; - this.SetScoreWorkTimingInterval = SetScoreWorkTimingInterval; - - // Backing tracks: - this.getBackingTrackList = getBackingTrackList; - this.SessionCloseBackingTrackFile = SessionCloseBackingTrackFile; - this.SessionOpenBackingTrackFile = SessionOpenBackingTrackFile; - this.SessionSetBackingTrackFileLoop = SessionSetBackingTrackFileLoop; - - // Metronome: - this.SessionCloseMetronome = SessionCloseMetronome; - this.SessionOpenMetronome = SessionOpenMetronome; - this.SessionSetMetronome = SessionSetMetronome; - this.setMetronomeOpenCallback = setMetronomeOpenCallback; - this.getMyNetworkState = getMyNetworkState; - this.getPeerState = getPeerState; - - // Client Update - this.IsAppInWritableVolume = IsAppInWritableVolume; - this.ClientUpdateVersion = ClientUpdateVersion; - this.ClientUpdateStartDownload = ClientUpdateStartDownload; - this.ClientUpdateStartUpdate = ClientUpdateStartUpdate; - - this.OpenSystemBrowser = OpenSystemBrowser; - this.RestartApplication = RestartApplication; - this.ShutdownApplication = ShutdownApplication; - - // Websocket/Auth sessions - this.OnLoggedIn = OnLoggedIn; - this.OnLoggedOut = OnLoggedOut; - - this.UserAttention = UserAttention; - this.IsFrontendVisible = IsFrontendVisible; - this.LastUsedProfileName = LastUsedProfileName; - this.SetLastUsedProfileName = SetLastUsedProfileName; - - // Recording Playback - this.GetLocalRecordingState = GetLocalRecordingState; - this.OpenRecording = OpenRecording; - this.CloseRecording = CloseRecording; - this.PreviewRecording = PreviewRecording; - this.ClosePreviewRecording = ClosePreviewRecording; - this.OnDownloadAvailable = OnDownloadAvailable; - - // Video functionality: - this.FTUESelectVideoCaptureDevice = FTUESelectVideoCaptureDevice - this.FTUESetVideoEncodeResolution = FTUESetVideoEncodeResolution; - this.FTUEGetVideoCaptureDeviceNames = FTUEGetVideoCaptureDeviceNames; - this.FTUECurrentSelectedVideoDevice = FTUECurrentSelectedVideoDevice; - this.FTUEGetAvailableEncodeVideoResolutions = FTUEGetAvailableEncodeVideoResolutions; - this.FTUEGetVideoCaptureDeviceCapabilities = FTUEGetVideoCaptureDeviceCapabilities; - this.FTUEGetSendFrameRates = FTUEGetSendFrameRates; - this.FTUESetSendFrameRates = FTUESetSendFrameRates; - this.GetCurrentVideoResolution = GetCurrentVideoResolution; - this.GetCurrentVideoFrameRate = GetCurrentVideoFrameRate; - - this.isSessVideoShared = isSessVideoShared; - this.SessStopVideoSharing = SessStopVideoSharing; - this.SessStartVideoSharing = SessStartVideoSharing; - - // Clipboard - this.SaveToClipboard = SaveToClipboard; - - // Capabilities - this.IsNativeClient = IsNativeClient; - - // Audio - this.IsAudioStarted = IsAudioStarted; - this.StopAudio = StopAudio; - this.ResetPageCounters = ResetPageCounters; - this.ReloadAudioSystem = ReloadAudioSystem; - - // Broadcasting - this.SessionLiveBroadcastStart = SessionLiveBroadcastStart; - this.SessionLiveBroadcastStop = SessionLiveBroadcastStop; - this.SessionSetConnectionStatusRefreshRate = SessionSetConnectionStatusRefreshRate; - - // fake calls; not a part of the actual jam client - this.RegisterP2PMessageCallbacks = RegisterP2PMessageCallbacks; - this.SetFakeRecordingImpl = SetFakeRecordingImpl; - - // network test - this.TestNetworkPktBwRate = TestNetworkPktBwRate; - this.StopNetworkTest = StopNetworkTest; - this.log = log; - this.getOperatingMode = getOperatingMode; - this.clientID = "devtester"; + function LastUsedProfileName() { + return 'default' }; + function SetLastUsedProfileName(name) { + return true; + }; + function log(level, message) { + console.log("beep : " + message) + } - })(window,jQuery); + function getOperatingMode() { + if (location.pathname == '/latency_tester') { + return 'server'; + } + else { + return 'client'; + } + } + + // passed an array of recording objects from the server + function GetLocalRecordingState(recordings) { + var result = {recordings: []}; + var recordingResults = result.recordings; + + var possibleAnswers = ['HQ', 'RT', 'MISSING', 'PARTIALLY_MISSING']; + + $.each(recordings, function (i, recording) { + // just make up a random yes-hq/yes-rt/missing answer + var recordingResult = {}; + recordingResult['aggregate_state'] = possibleAnswers[Math.floor((Math.random() * 4))]; + recordingResults.push(recordingResult); + }) + + return result; + } + + function OpenRecording(claimedRecording) { + return {success: true} + } + + function PreviewRecording(claimedRecording) { + return OpenRecording(claimedRecording); + } + + function CloseRecording() { + } + + function ClosePreviewRecording() { + CloseRecording(); + } + + function OnDownloadAvailable() { + } + + function SaveToClipboard(text) { + } + + function IsNativeClient() { /* must always return false in all scenarios due to not ruin scoring !*/ + return false; + } + + function IsAudioStarted() { + logger.debug("FakeJamClient: IsAudioStarted"); + return false; + } + + function StopAudio() { + logger.debug("FakeJamClient: StopAudio"); + } + + function ResetPageCounters() { + logger.debug("FakeJamClient: ResetPageCounters"); + } + + function ReloadAudioSystem(leaveRunning, loadLastProfile, reinitAssignments) { + logger.debug("FakeJamClient: ReloadAudioSystem"); + return {}; + } + + function SessionLiveBroadcastStart(host, port, mount, sourceUser, sourcePass, preferredClientId, bitrate) { + logger.debug("SessionLiveBroadcastStart requested"); + } + + function SessionLiveBroadcastStop() { + logger.debug("SessionLiveBroadcastStop requested"); + } + + function RegisterQuitCallback() { + } + + function LeaveSessionAndMinimize() { + } + + function SetAutoStart() { + } + + function GetAutoStart() { + return true; + } + + function SaveSettings() { + } + + function GetUseStaticPort() { + return false; + } + + function SetUseStaticPort() { + } + + function GetStaticPort() { + return 0 + } + + function SetStaticPort() { + } + + + function VSTLoad() { + } + + function VSTScan(callback) { + setTimeout(eval(callback + "()"), 1000) + } + + function hasVstHost() { + return false; + } + + function getPluginList() { + return {vsts: []} + } + + function clearPluginList() { + } + + function listTrackAssignments() { + return {} + } + + // Javascript Bridge seems to camel-case + // Set the instance functions: + this.AbortRecording = AbortRecording; + this.OnTrySyncCommand = OnTrySyncCommand; + this.GetRecordingManagerState = GetRecordingManagerState; + this.GetASIODevices = GetASIODevices; + this.GetOS = GetOS; + this.GetOSAsString = GetOSAsString; + this.JoinSession = JoinSession; + this.LatencyUpdated = LatencyUpdated; + this.LeaveSession = LeaveSession; + this.P2PMessageReceived = P2PMessageReceived; + this.ParticipantJoined = ParticipantJoined; + this.ParticipantLeft = ParticipantLeft; + this.RecordTestBegin = RecordTestBegin; + this.RecordTestEnd = RecordTestEnd; + this.RecordTestPlayback = RecordTestPlayback; + this.RegisterVolChangeCallBack = RegisterVolChangeCallBack; + this.SendP2PMessage = SendP2PMessage; + this.SetASIOEnabled = SetASIOEnabled; + this.SignalLatencyUpdated = SignalLatencyUpdated; + this.SignalSendP2PMessage = SignalSendP2PMessage; + this.StartPlayTest = StartPlayTest; + this.StartRecordTest = StartRecordTest; + this.StartRecording = StartRecording; + this.StopPlayTest = StopPlayTest; + this.StopRecording = StopRecording; + this.TestASIOLatency = TestASIOLatency; + this.TestLatency = TestLatency; + this.IsMyNetworkWireless = IsMyNetworkWireless; + this.SetNetworkTestScore = SetNetworkTestScore; + this.GetNetworkTestScore = GetNetworkTestScore; + this.SetVideoNetworkTestScore = SetVideoNetworkTestScore; + this.GetVideoNetworkTestScore = GetVideoNetworkTestScore; + this.SetLatencyTestBlocked = SetLatencyTestBlocked; + this.isLatencyTestBlocked = isLatencyTestBlocked; + this.GetLastLatencyTestTimes = GetLastLatencyTestTimes; + this.RegisterQuitCallback = RegisterQuitCallback; + this.LeaveSessionAndMinimize = LeaveSessionAndMinimize; + this.GetAutoStart = GetAutoStart; + this.SetAutoStart = SetAutoStart; + this.GetUseStaticPort = GetUseStaticPort; + this.SetUseStaticPort = SetUseStaticPort; + this.GetStaticPort = GetStaticPort; + this.SetStaticPort = SetStaticPort; + this.connected = true; + + // FTUE (round 3) + this.FTUESetInputMusicDevice = FTUESetInputMusicDevice; + this.FTUESetOutputMusicDevice = FTUESetOutputMusicDevice; + this.FTUEGetInputMusicDevice = FTUEGetInputMusicDevice; + this.FTUEGetOutputMusicDevice = FTUEGetOutputMusicDevice; + this.FTUEGetChatInputVolume = FTUEGetChatInputVolume; + this.FTUEGetChatInputs = FTUEGetChatInputs; + this.FTUEGetChannels = FTUEGetChannels; + this.FTUEGetAudioDevices = FTUEGetAudioDevices; + this.FTUEStartIoPerfTest = FTUEStartIoPerfTest; + this.FTUEGetIoPerfData = FTUEGetIoPerfData; + this.FTUEGetDevices = FTUEGetDevices; + this.FTUEGetFrameSize = FTUEGetFrameSize; + this.FTUECancel = FTUECancel; + this.FTUEPageEnter = FTUEPageEnter; + this.FTUEPageLeave = FTUEPageLeave; + this.FTUEGetMusicProfileName = FTUEGetMusicProfileName; + this.FTUESetMusicProfileName = FTUESetMusicProfileName; + this.FTUESetPreferredMixerSampleRate = FTUESetPreferredMixerSampleRate; + this.FTUESetPreferredOutputSampleRate = FTUESetPreferredOutputSampleRate; + this.FTUESetPreferredChatSampleRate = FTUESetPreferredChatSampleRate; + this.FTUEgetInputDeviceSampleRate = FTUEgetInputDeviceSampleRate; + this.FTUEgetOutputDeviceSampleRate = FTUEgetOutputDeviceSampleRate; + this.FTUEGetInputLatency = FTUEGetInputLatency; + this.FTUEGetInputVolume = FTUEGetInputVolume; + this.FTUEGetMusicInputs = FTUEGetMusicInputs; + this.FTUEGetMusicOutputs = FTUEGetMusicOutputs; + this.FTUEGetOutputLatency = FTUEGetOutputLatency; + this.FTUEGetOutputVolume = FTUEGetOutputVolume; + this.FTUEGetStatus = FTUEGetStatus; + this.FTUEGetVolumeRanges = FTUEGetVolumeRanges; + this.FTUEHasControlPanel = FTUEHasControlPanel; + this.FTUEInit = FTUEInit; + this.FTUEOpenControlPanel = FTUEOpenControlPanel; + this.FTUERegisterLatencyCallback = FTUERegisterLatencyCallback; + this.FTUERegisterVUCallbacks = FTUERegisterVUCallbacks; + this.FTUERefreshDevices = FTUERefreshDevices; + this.FTUESave = FTUESave; + this.FTUESetChatInput = FTUESetChatInput; + this.FTUESetChatInputVolume = FTUESetChatInputVolume; + this.FTUESetInputVolume = FTUESetInputVolume; + this.FTUESetFrameSize = FTUESetFrameSize; + this.FTUESetInputLatency = FTUESetInputLatency; + this.FTUESetMusicDevice = FTUESetMusicDevice; + this.FTUESetMusicInput = FTUESetMusicInput; + this.FTUESetMusicOutput = FTUESetMusicOutput; + this.FTUESetOutputLatency = FTUESetOutputLatency; + this.FTUESetOutputVolume = FTUESetOutputVolume; + this.FTUESetStatus = FTUESetStatus; + this.FTUEStartLatency = FTUEStartLatency; + this.FTUEGetExpectedLatency = FTUEGetExpectedLatency; + this.FTUEGetGoodConfigurationList = FTUEGetGoodConfigurationList; + this.FTUEGetAllAudioConfigurations = FTUEGetAllAudioConfigurations; + this.FTUEGetGoodAudioConfigurations = FTUEGetGoodAudioConfigurations; + this.FTUEGetConfigurationDevice = FTUEGetConfigurationDevice; + this.FTUEIsMusicDeviceWDM = FTUEIsMusicDeviceWDM; + this.FTUELoadAudioConfiguration = FTUELoadAudioConfiguration; + this.FTUEClearChannelAssignments = FTUEClearChannelAssignments; + this.FTUEClearChatInput = FTUEClearChatInput; + this.FTUECreateUpdatePlayBackProfile = FTUECreateUpdatePlayBackProfile; + + // Session + this.SessionAddTrack = SessionAddTrack; + this.SessionCurrrentJamTrackPlayPosMs = SessionCurrrentJamTrackPlayPosMs; + this.SessionGetJamTracksPlayDurationMs = SessionGetJamTracksPlayDurationMs; + this.SessionGetControlState = SessionGetControlState; + this.SessionGetAllControlState = SessionGetAllControlState; + this.SessionSetUserName = SessionSetUserName; + this.SessionGetIDs = SessionGetIDs; + this.RegisterRecordingManagerCallbacks = RegisterRecordingManagerCallbacks; + this.RegisterRecordingCallbacks = RegisterRecordingCallbacks; + this.SessionRegisterCallback = SessionRegisterCallback; + this.SessionSetAlertCallback = SessionSetAlertCallback; + this.SessionSetControlState = SessionSetControlState; + this.SessionSetRecordingFilename = SessionSetRecordingFilename; + this.SessionSetRecordingFolder = SessionSetRecordingFolder; + this.SessionStartPlay = SessionStartPlay; + this.SessionStartRecording = SessionStartRecording; + this.SessionStopPlay = SessionStopPlay; + this.SessionAddPlayTrack = SessionAddPlayTrack; + this.SessionRemoveAllPlayTracks = SessionRemoveAllPlayTracks; + this.SessionStopRecording = SessionStopRecording; + this.isSessionTrackPlaying = isSessionTrackPlaying; + this.SessionCurrrentPlayPosMs = SessionCurrrentPlayPosMs; + this.SessionGetTracksPlayDurationMs = SessionGetTracksPlayDurationMs; + this.SessionPageEnter = SessionPageEnter; + this.SessionPageLeave = SessionPageLeave; + this.SetMixerMode = SetMixerMode; + + this.SetVURefreshRate = SetVURefreshRate; + this.SessionGetMasterLocalMix = SessionGetMasterLocalMix; + this.SessionSetMasterLocalMix = SessionSetMasterLocalMix; + this.SessionGetDeviceLatency = SessionGetDeviceLatency; + this.SessionAudioResync = SessionAudioResync; + + // Track + this.TrackGetChannels = TrackGetChannels; + this.TrackSetAssignment = TrackSetAssignment; + this.TrackGetAssignment = TrackGetAssignment; + this.TrackSetCount = TrackSetCount; + this.TrackGetCount = TrackGetCount; + this.TrackSave = TrackSave; + this.TrackLoadAssignments = TrackLoadAssignments; + this.TrackSetInstrument = TrackSetInstrument; + this.TrackGetInstrument = TrackGetInstrument; + this.TrackSaveAssignments = TrackSaveAssignments; + + this.TrackGetDevices = TrackGetDevices; + this.TrackDeleteProfile = TrackDeleteProfile; + this.TrackGetMusicDeviceID = TrackGetMusicDeviceID; + this.TrackSetMusicDevice = TrackSetMusicDevice; + this.TrackGetMusicDeviceNames = TrackGetMusicDeviceNames; + this.TrackHasControlPanel = TrackHasControlPanel; + this.TrackOpenControlPanel = TrackOpenControlPanel; + this.TrackIsMusicDeviceType = TrackIsMusicDeviceType; + + this.TrackGetChatEnable = TrackGetChatEnable; + this.TrackSetChatEnable = TrackSetChatEnable; + + this.TrackGetChatUsesMusic = TrackGetChatUsesMusic; + this.TrackSetChatUsesMusic = TrackSetChatUsesMusic; + + this.GetJamTrackSettings = GetJamTrackSettings; + this.JamTrackStopPlay = JamTrackStopPlay; + this.JamTrackPlay = JamTrackPlay; + this.JamTrackIsPlayable = JamTrackIsPlayable; + this.JamTrackGetTrackDetail = JamTrackGetTrackDetail; + this.JamTrackKeysRequest = JamTrackKeysRequest; + this.JamTrackDownload = JamTrackDownload; + + // Scoring Knobs + this.GetScoreWorkTimingInterval = GetScoreWorkTimingInterval; + this.SetScoreWorkTimingInterval = SetScoreWorkTimingInterval; + + // Backing tracks: + this.getBackingTrackList = getBackingTrackList; + this.SessionCloseBackingTrackFile = SessionCloseBackingTrackFile; + this.SessionOpenBackingTrackFile = SessionOpenBackingTrackFile; + this.SessionSetBackingTrackFileLoop = SessionSetBackingTrackFileLoop; + + // Metronome: + this.SessionCloseMetronome = SessionCloseMetronome; + this.SessionOpenMetronome = SessionOpenMetronome; + this.SessionSetMetronome = SessionSetMetronome; + this.setMetronomeOpenCallback = setMetronomeOpenCallback; + this.getMyNetworkState = getMyNetworkState; + this.getPeerState = getPeerState; + + // Client Update + this.IsAppInWritableVolume = IsAppInWritableVolume; + this.ClientUpdateVersion = ClientUpdateVersion; + this.ClientUpdateStartDownload = ClientUpdateStartDownload; + this.ClientUpdateStartUpdate = ClientUpdateStartUpdate; + + this.getClientParentChildRole = getClientParentChildRole; + this.getParentClientId = getParentClientId; + this.OpenSystemBrowser = OpenSystemBrowser; + this.RestartApplication = RestartApplication; + this.ShutdownApplication = ShutdownApplication; + + // Websocket/Auth sessions + this.OnLoggedIn = OnLoggedIn; + this.OnLoggedOut = OnLoggedOut; + + this.UserAttention = UserAttention; + this.IsFrontendVisible = IsFrontendVisible; + this.LastUsedProfileName = LastUsedProfileName; + this.SetLastUsedProfileName = SetLastUsedProfileName; + + // Recording Playback + this.GetLocalRecordingState = GetLocalRecordingState; + this.OpenRecording = OpenRecording; + this.CloseRecording = CloseRecording; + this.PreviewRecording = PreviewRecording; + this.ClosePreviewRecording = ClosePreviewRecording; + this.OnDownloadAvailable = OnDownloadAvailable; + + // Video functionality: + this.testVideoRender = testVideoRender; + this.FTUESelectVideoCaptureDevice = FTUESelectVideoCaptureDevice; + this.FTUESetVideoEncodeResolution = FTUESetVideoEncodeResolution; + this.FTUEGetVideoCaptureDeviceNames = FTUEGetVideoCaptureDeviceNames; + this.FTUECurrentSelectedVideoDevice = FTUECurrentSelectedVideoDevice; + this.FTUEGetAvailableEncodeVideoResolutions = FTUEGetAvailableEncodeVideoResolutions; + this.FTUEGetVideoCaptureDeviceCapabilities = FTUEGetVideoCaptureDeviceCapabilities; + this.FTUEGetSendFrameRates = FTUEGetSendFrameRates; + this.FTUESetSendFrameRates = FTUESetSendFrameRates; + this.GetCurrentVideoResolution = GetCurrentVideoResolution; + this.GetCurrentVideoFrameRate = GetCurrentVideoFrameRate; + this.GetSampleRate = GetSampleRate; + this.FTUESetVideoShareEnable = FTUESetVideoShareEnable; + this.FTUEGetVideoShareEnable = FTUEGetVideoShareEnable; + this.isSessVideoShared = isSessVideoShared; + this.SessStopVideoSharing = SessStopVideoSharing; + this.SessStartVideoSharing = SessStartVideoSharing; + + // Clipboard + this.SaveToClipboard = SaveToClipboard; + + // Capabilities + this.IsNativeClient = IsNativeClient; + + // Audio + this.IsAudioStarted = IsAudioStarted; + this.StopAudio = StopAudio; + this.ResetPageCounters = ResetPageCounters; + this.ReloadAudioSystem = ReloadAudioSystem; + + // Broadcasting + this.SessionLiveBroadcastStart = SessionLiveBroadcastStart; + this.SessionLiveBroadcastStop = SessionLiveBroadcastStop; + this.SessionSetConnectionStatusRefreshRate = SessionSetConnectionStatusRefreshRate; + + // fake calls; not a part of the actual jam client + this.RegisterP2PMessageCallbacks = RegisterP2PMessageCallbacks; + this.SetFakeRecordingImpl = SetFakeRecordingImpl; + + // network test + this.TestNetworkPktBwRate = TestNetworkPktBwRate; + this.StopNetworkTest = StopNetworkTest; + this.log = log; + this.getOperatingMode = getOperatingMode; + this.VSTLoad = VSTLoad; + this.VSTScan = VSTScan; + this.hasVstHost = hasVstHost; + this.getPluginList = getPluginList; + this.clearPluginList = clearPluginList; + this.listTrackAssignments = listTrackAssignments; + this.clientID = "devtester"; + }; + +})(window, jQuery); diff --git a/web/app/assets/javascripts/feedHelper.js b/web/app/assets/javascripts/feedHelper.js index cdf32c7f9..61f7b69e8 100644 --- a/web/app/assets/javascripts/feedHelper.js +++ b/web/app/assets/javascripts/feedHelper.js @@ -454,7 +454,6 @@ mix_class: feed['has_mix?'] ? 'has-mix' : 'no-mix', } - console.log("OPTIONS", options) var $feedItem = $(context._.template($('#template-feed-recording').html(), options, {variable: 'data'})); var $controls = $feedItem.find('.recording-controls'); @@ -471,6 +470,43 @@ $controls.data('server-info', feed.mix) // for recordingUtils helper methods $controls.data('view-context', 'feed') + // tack on video if available + if(feed.external_video_id) { + + var $videoWrapper = $feedItem.find('.video-wrapper') + var $videoContainer = $feedItem.find('.video-container') + if(gon.isNativeClient) { + var $embed = $('' + + '' + + '') + $videoContainer.append($embed).addClass('no-embed') + $videoWrapper.removeClass('hidden') + $embed.click(function() { + context.JK.popExternalLink($(this).attr('href')) + return false; + }) + } + else { + var $embed = $('