diff --git a/admin/Gemfile b/admin/Gemfile
index a809dd0ee..11cb64736 100644
--- a/admin/Gemfile
+++ b/admin/Gemfile
@@ -43,6 +43,7 @@ gem 'jquery-rails' # , '2.3.0' # pinned because jquery-ui-rails was split from j
gem 'jquery-ui-rails', '4.2.1'
gem 'rails3-jquery-autocomplete'
gem 'activeadmin' #, github: 'activeadmin', branch: '0-6-stable'
+#gem 'activeadmin', github: 'activeadmin
gem 'mime-types', '1.25'
gem 'meta_search'
gem 'fog'
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/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/db/manifest b/db/manifest
index 685148214..53fd01705 100755
--- a/db/manifest
+++ b/db/manifest
@@ -357,4 +357,6 @@ track_user_on_lesson.sql
audio_in_music_notations.sql
lesson_time_tracking.sql
packaged_test_drive.sql
-packaged_test_drive2.sql
\ No newline at end of file
+packaged_test_drive2.sql
+jamclass_report.sql
+jamblasters_network.sql
\ 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/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb
index 1e1aab60f..b76e749f2 100755
--- a/ruby/lib/jam_ruby.rb
+++ b/ruby/lib/jam_ruby.rb
@@ -303,6 +303,8 @@ 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/jam_track_importer.rb b/ruby/lib/jam_ruby/jam_track_importer.rb
index 3032e8b12..e296cae3c 100644
--- a/ruby/lib/jam_ruby/jam_track_importer.rb
+++ b/ruby/lib/jam_ruby/jam_track_importer.rb
@@ -505,6 +505,11 @@ 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'
@@ -550,7 +555,7 @@ module JamRuby
song = metalocation[(first_dash+3)..-1].strip
bits << song
- elsif is_tency_storage? || is_tim_tracks_storage?
+ elsif is_tency_storage? || is_tim_tracks_storage? || is_helbing_storage?
suffix = '/meta.yml'
@@ -582,6 +587,9 @@ module JamRuby
if is_tim_tracks_storage?
song = metalocation[(first_dash+3)..-1].strip
bits << song
+ elsif is_helbing_storage?
+ song = metalocation[(first_dash+3)..-1].strip
+ bits << song
elsif is_tency_storage?
last_dash = metalocation.rindex('-')
if last_dash
@@ -881,6 +889,9 @@ module JamRuby
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')
@@ -2231,6 +2242,8 @@ module JamRuby
tim_tracks_s3_manager
elsif is_drumma_storage?
drumma_s3_manager
+ elsif is_helbing_storage?
+ helbing_s3_manager
else
s3_manager
end
@@ -2256,6 +2269,10 @@ module JamRuby
@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
@@ -2328,6 +2345,12 @@ module JamRuby
@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
@@ -2418,6 +2441,22 @@ module JamRuby
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|
@@ -2435,6 +2474,10 @@ module JamRuby
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)
@@ -3392,7 +3435,10 @@ module JamRuby
if is_tim_tracks_storage?
meta[:genres] = ['acapella']
+ elsif is_helbing_storage?
+ meta[:genres] = ['jazz']
end
+
meta
rescue AWS::S3::Errors::NoSuchKey
return nil
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/connection.rb b/ruby/lib/jam_ruby/models/connection.rb
index 5b1444fc7..862a7c0d2 100644
--- a/ruby/lib/jam_ruby/models/connection.rb
+++ b/ruby/lib/jam_ruby/models/connection.rb
@@ -20,6 +20,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]}
@@ -87,6 +88,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?
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/jamblaster.rb b/ruby/lib/jam_ruby/models/jamblaster.rb
index 7b6d051fc..b1e19bfc1 100644
--- a/ruby/lib/jam_ruby/models/jamblaster.rb
+++ b/ruby/lib/jam_ruby/models/jamblaster.rb
@@ -8,7 +8,7 @@ module JamRuby
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
diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb
index 04fd37f4c..4723e5588 100644
--- a/ruby/lib/jam_ruby/models/user.rb
+++ b/ruby/lib/jam_ruby/models/user.rb
@@ -42,7 +42,6 @@ module JamRuby
# updating_password corresponds to a lost_password
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
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/web/Gemfile b/web/Gemfile
index df30e75e5..79e366455 100644
--- a/web/Gemfile
+++ b/web/Gemfile
@@ -102,6 +102,13 @@ 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', '0.3.0'
gem 'rails-assets-classnames'
diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js
index 3954949c6..cfe9d7a3c 100644
--- a/web/app/assets/javascripts/globals.js
+++ b/web/app/assets/javascripts/globals.js
@@ -63,7 +63,8 @@
PREVIEW_PLAYED: 'preview_played',
VST_OPERATION_SELECTED: 'vst_operation_selected',
VST_EFFECT_SELECTED: 'vst_effect_selected',
- LESSON_SESSION_ACTION: 'lesson_session_action'
+ LESSON_SESSION_ACTION: 'lesson_session_action',
+ JAMBLASTER_ACTION: 'jamblaster_action'
};
context.JK.PLAYBACK_MONITOR_MODE = {
diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js
index bd8dd4f7a..ea15e3e00 100644
--- a/web/app/assets/javascripts/jam_rest.js
+++ b/web/app/assets/javascripts/jam_rest.js
@@ -595,6 +595,19 @@
return detail;
}
+ function getUserJamBlasters(options) {
+ if(!options) {
+ options = {}
+ }
+ var id = getId(options);
+ return $.ajax({
+ type: "GET",
+ dataType: "json",
+ url: "/api/users/" + id + '/jamblasters?' + $.param(options),
+ processData: false
+ });
+ }
+
function getUserProfile(options) {
if (!options) {
options = {}
@@ -2550,6 +2563,7 @@
this.cancelSession = cancelSession;
this.updateScheduledSession = updateScheduledSession;
this.getUserDetail = getUserDetail;
+ this.getUserJamBlasters = getUserJamBlasters;
this.getUserAuthorizations = getUserAuthorizations;
this.getGoogleAuth = getGoogleAuth;
this.getUserProfile = getUserProfile;
diff --git a/web/app/assets/javascripts/jquery.jamblasterOptions.js b/web/app/assets/javascripts/jquery.jamblasterOptions.js
new file mode 100644
index 000000000..9f947359e
--- /dev/null
+++ b/web/app/assets/javascripts/jquery.jamblasterOptions.js
@@ -0,0 +1,79 @@
+(function(context, $) {
+
+ "use strict";
+
+ context.JK = context.JK || {};
+
+
+ // creates an iconic/graphical instrument selector. useful when there is minimal real-estate
+
+ $.fn.jamblasterOptions = function(options) {
+
+ return this.each(function(index) {
+
+ function close() {
+ $parent.btOff();
+ $parent.focus();
+ }
+
+ var $parent = $(this);
+ if($parent.data('jamblasterOptions')) {
+ //return;
+ }
+
+ $parent.data('jamblasterOptions', options)
+ function onJamBlasterOptionSelected() {
+ var $li = $(this);
+ var option = $li.attr('data-jamblaster-option');
+
+ close();
+ $parent.triggerHandler(context.JK.EVENTS.JAMBLASTER_ACTION, {option: option, options: $parent.data('jamblasterOptions')});
+ return false;
+ };
+
+ // if the user goes into the bubble, remove
+ function waitForBubbleHover($bubble) {
+ $bubble.hoverIntent({
+ over: function() {
+ if(timeout) {
+ clearTimeout(timeout);
+ timeout = null;
+ }
+ },
+ out: function() {
+ $parent.btOff();
+ }});
+ }
+
+ var timeout = null;
+
+ var html = context._.template($('#template-jamblaster-options').html(), options, { variable: 'data' })
+
+ context.JK.hoverBubble($parent, html, {
+ trigger:'none',
+ cssClass: 'jamblaster-options-popup',
+ spikeGirth:0,
+ spikeLength:0,
+ overlap: -10,
+ width:120,
+ closeWhenOthersOpen: true,
+ offsetParent: $parent.closest('.screen'),
+ positions:['bottom'],
+ preShow: function() {
+
+ },
+ postShow:function(container) {
+ $(container).find('li').click(onJamBlasterOptionSelected)
+ if(timeout) {
+ clearTimeout(timeout);
+ timeout = null;
+ }
+ waitForBubbleHover($(container))
+ timeout = setTimeout(function() {$parent.btOff()}, 6000)
+ }
+ });
+ });
+ }
+
+
+})(window, jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/jquery.lessonSessionActions.js b/web/app/assets/javascripts/jquery.lessonSessionActions.js
index 85db4c285..86859618e 100644
--- a/web/app/assets/javascripts/jquery.lessonSessionActions.js
+++ b/web/app/assets/javascripts/jquery.lessonSessionActions.js
@@ -12,7 +12,7 @@
return this.each(function(index) {
function close() {
- //$parent.btOff();
+ $parent.btOff();
$parent.focus();
}
@@ -41,7 +41,7 @@
}
},
out: function() {
- //$parent.btOff();
+ $parent.btOff();
}});
}
@@ -103,7 +103,7 @@
timeout = null;
}
waitForBubbleHover($(container))
- //timeout = setTimeout(function() {$parent.btOff()}, 6000)
+ timeout = setTimeout(function() {$parent.btOff()}, 6000)
}
});
});
diff --git a/web/app/assets/javascripts/landing/landing.js b/web/app/assets/javascripts/landing/landing.js
index 3cc86db70..b979e6786 100644
--- a/web/app/assets/javascripts/landing/landing.js
+++ b/web/app/assets/javascripts/landing/landing.js
@@ -22,6 +22,7 @@
//= require jquery.exists
//= require jquery.manageVsts
//= require jquery.lessonSessionActions
+//= require jquery.jamblasterOptions
//= require ResizeSensor
//= require AAA_Log
//= require AAC_underscore
diff --git a/web/app/assets/javascripts/react-components/CheckBoxList.js.jsx.coffee b/web/app/assets/javascripts/react-components/CheckBoxList.js.jsx.coffee
index 2364c52c5..3f9e62b31 100644
--- a/web/app/assets/javascripts/react-components/CheckBoxList.js.jsx.coffee
+++ b/web/app/assets/javascripts/react-components/CheckBoxList.js.jsx.coffee
@@ -44,7 +44,7 @@ logger = context.JK.logger
for object in this.props.sourceObjects
nm = "check_#{object.id}"
checked = @isChecked(object.id)
- object_options.push `
`
+ object_options.push `
`
`
diff --git a/web/app/assets/javascripts/react-components/ConfigureTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/ConfigureTracks.js.jsx.coffee
index dac708c9b..f23f1fb57 100644
--- a/web/app/assets/javascripts/react-components/ConfigureTracks.js.jsx.coffee
+++ b/web/app/assets/javascripts/react-components/ConfigureTracks.js.jsx.coffee
@@ -41,11 +41,11 @@ MIDI_TRACK = context.JK.MIDI_TRACK
for midiDevice in @state.configureTracks.attachedMidiDevices.midiDevices
if midiDevice.deviceIndex == inputsForTrack.vst?.midiDeviceIndex
midiDeviceName = midiDevice.deviceName
- inputs.push(`
{midiDeviceName}
`)
+ inputs.push(`
{midiDeviceName}
`)
else
trackTypeLabel = 'AUDIO'
for input in inputsForTrack
- inputs.push(`
{input.name}
`)
+ inputs.push(`
{input.name}
`)
if !inputsForTrack.instrument_id?
instrument = `?`
diff --git a/web/app/assets/javascripts/react-components/JamBlasterNameDialog.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamBlasterNameDialog.js.jsx.coffee
new file mode 100644
index 000000000..33d68f43e
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/JamBlasterNameDialog.js.jsx.coffee
@@ -0,0 +1,92 @@
+context = window
+@JamBlasterNameDialog = React.createClass({
+
+ mixins: [Reflux.listenTo(@AppStore, "onAppInit")]
+ teacher: null
+
+ beforeShow: (args) ->
+ logger.debug("JamBlasterNameDialog.beforeShow", args.d1)
+ @setState({name: args.d1})
+
+
+ afterHide: () ->
+
+ onAppInit: (@app) ->
+ dialogBindings = {
+ 'beforeShow': @beforeShow,
+ 'afterHide': @afterHide
+ };
+
+ @app.bindDialog('jamblaster-name-dialog', dialogBindings);
+
+ getInitialState: () ->
+ {
+ name: ''
+ }
+
+
+ componentDidMount: () ->
+ @root = $(@getDOMNode())
+ @dialog = @root.closest('.dialog')
+
+ doCancel: (e) ->
+ e.preventDefault()
+ @app.layout.closeDialog('jamblaster-name-dialog', true);
+
+ onNameChange: (e) ->
+
+ @setState({name: $(e.target).val()})
+
+ updateName: (e) ->
+ e.preventDefault()
+
+ # validate
+
+ name = @root.find('.name').val()
+
+ characterMatch = /^[a-z0-9,' -]+$/i
+
+ if name.length == 0 || name == ''
+ context.JK.Banner.showAlert('invalid name', 'Please specify a name.')
+ return
+ else if name.length < 2
+ context.JK.Banner.showAlert('invalid name', 'Please specify a name at least 3 characters long.')
+ return
+ else if name.length > 63
+ context.JK.Banner.showAlert('invalid name', 'The name must be less than 64 characters long.')
+ return
+ else if characterMatch.test(name)
+ context.JK.Banner.showAlert('invalid name',
+ 'The can only contain A-Z, 0-9, commas, apostrophes, spaces, or hyphens.')
+ return
+
+ result = context.jamClient.setJBName(name.trim())
+
+ if !result
+ context.JK.Banner.showAlert('unable to set the name',
+ 'Please email support@jamkazam.com with the name you are trying to set, or refresh the page and try again.')
+ else
+ @app.layout.closeDialog('jamblaster-name-dialog')
+ render: () ->
+ `
+
+
+
+
update name of JamBlaster
+
+
+
+
You can change the display name for this JamBlaster. The name can only contain A-Z, 0-9, commas, apostrophes,
+ spaces, or hyphens. A valid example: "John Doe's JamBlaster"
To connect this application/device with the selected JamBlaster, please click the Connect button below, and then push the small black plastic button located on the back of the JamBlaster between the USB and power ports to confirm this pairing within 60 seconds of clicking the Connect button below.
+
+ {message}
+
+ {actions}
+
+
`
+
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/JamBlasterScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamBlasterScreen.js.jsx.coffee
new file mode 100644
index 000000000..45c8e1ba2
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/JamBlasterScreen.js.jsx.coffee
@@ -0,0 +1,184 @@
+context = window
+rest = context.JK.Rest()
+logger = context.JK.logger
+
+@JamBlasterScreen = React.createClass({
+
+ mixins: [
+ @ICheckMixin,
+ @PostProcessorMixin,
+ @BonjourMixin,
+ Reflux.listenTo(AppStore, "onAppInit"),
+ Reflux.listenTo(UserStore, "onUserChanged")
+ ]
+
+ TILE_AUDIO: 'audio'
+ TILE_INTERNET: 'internet'
+ TILE_MANAGEMENT: 'management'
+ TILE_USB: 'usb'
+
+ TILES: ['audio', 'internet', 'management', 'usb']
+
+ onAppInit: (@app) ->
+ @app.bindScreen('jamblaster',
+ {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide})
+
+ onUserChanged: (userState) ->
+ @setState({user: userState?.user})
+
+ componentDidMount: () ->
+ @root = $(@getDOMNode())
+
+ componentWillUpdate: (nextProps, nextState) ->
+
+ componentDidUpdate: () ->
+ items = @root.find('.jamtable .optionsColumn .jamblaster-options-btn')
+
+ $.each(items, (i, node) => (
+ $node = $(node)
+
+ jamblaster = @findJamBlaster($node.attr('data-jamblaster-id'))
+
+ $node.jamblasterOptions(jamblaster).off(context.JK.EVENTS.JAMBLASTER_ACTION).on(context.JK.EVENTS.JAMBLASTER_ACTION,
+ @jamblasterOptionSelected)
+ ))
+#context.JK.popExternalLinks(@root)
+
+ jamblasterOptionSelected: (e, data) ->
+ jamblasterId = data.options.id
+ jamblaster = @findJamBlaster(jamblasterId)
+
+ if data.option == 'auto-connect'
+ context.JK.Banner.showNotice('Auto-Connect',
+ 'Auto-Connect is always on by default. It can not currently configurable.')
+ else if data.option == 'restart'
+ context.JK.Banner.showNotice('Restart',
+ 'To restart the JamBlaster, you must manually cycle power (unplug, then plug).')
+ else if data.option == 'name'
+ context.layout.showDialog('jamblaster-name-dialog').one(context.JK.EVENTS.DIALOG_CLOSED, (e, data) =>
+ @resyncBonjour()
+ )
+
+ else if data.option == 'check-for-updates'
+ context.JK.Banner.showNotice('Check for Update',
+ 'The JamBlaster only checks for updates on start up. Please reboot the JamBlaster')
+ else if data.option == 'set-static-ports'
+ context.layout.showDialog('jamblaster-port-dialog')
+ else if data.option == 'factory-reset'
+ context.JK.Banner.showNotice('Factory Reset',
+ 'The JamBlaster only checks for updates when it boots up, and if there is an update available, it will automatically begin updating.
If you don't see your JamBlaster listed above, please check to make sure you have power connected to your JamBlaster,
+ and make sure your JamBlaster is connected via an Ethernet cable to the same router/network as the device on which you are viewing this application.
+