diff --git a/admin/Gemfile b/admin/Gemfile index 2e1799c5d..a2544488b 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -109,6 +109,7 @@ group :development, :test do gem 'database_cleaner', '0.7.0' gem 'launchy' gem 'faker', '1.3.0' + gem 'puma' end group :test do diff --git a/admin/app/admin/broadcast_notifications.rb b/admin/app/admin/broadcast_notifications.rb new file mode 100644 index 000000000..211d9298a --- /dev/null +++ b/admin/app/admin/broadcast_notifications.rb @@ -0,0 +1,26 @@ +ActiveAdmin.register JamRuby::BroadcastNotification, :as => 'BroadcastNotification' do + + menu :label => 'Notifications' + + config.sort_order = 'created_at_desc' + config.batch_actions = false + config.clear_action_items! + config.filters = false + + action_item :only => :index do + link_to "New Broadcast" , "broadcast_notifications/new" + end + + show do + attributes_table do + row :title + row :message + row :button_label + row :button_url + row :frequency + row :frequency_distribution + end + end + + +end diff --git a/db/Gemfile.lock b/db/Gemfile.lock index eb6aee107..8d6d039c2 100644 --- a/db/Gemfile.lock +++ b/db/Gemfile.lock @@ -16,3 +16,6 @@ PLATFORMS DEPENDENCIES pg_migrate (= 0.1.13) + +BUNDLED WITH + 1.10.3 diff --git a/db/manifest b/db/manifest index 1fecbf802..9d147ec77 100755 --- a/db/manifest +++ b/db/manifest @@ -286,3 +286,4 @@ signing.sql optimized_redeemption.sql optimized_redemption_warn_mode.sql affiliate_partners2.sql +broadcast_notifications.sql \ No newline at end of file diff --git a/db/up/broadcast_notifications.sql b/db/up/broadcast_notifications.sql new file mode 100644 index 000000000..5a7cb80aa --- /dev/null +++ b/db/up/broadcast_notifications.sql @@ -0,0 +1,22 @@ +CREATE TABLE broadcast_notifications ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + title VARCHAR(64), + message VARCHAR(256), + button_label VARCHAR(32), + button_url VARCHAR, + frequency INTEGER DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE broadcast_notification_views ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id varchar(64) NOT NULL REFERENCES users(id), + broadcast_notification_id varchar(64) NOT NULL REFERENCES broadcast_notifications(id) ON DELETE CASCADE, + view_count INTEGER DEFAULT 0, + active_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX user_broadcast_idx ON broadcast_notification_views(user_id, broadcast_notification_id); diff --git a/ruby/Gemfile b/ruby/Gemfile index 8ef539d9a..621108eea 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -62,6 +62,7 @@ group :test do gem 'resque_spec' #, :path => "/home/jam/src/resque_spec/" gem 'timecop' gem 'rspec-prof' + gem 'byebug' 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 51e887dc5..8c1a51a58 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -225,6 +225,8 @@ require "jam_ruby/models/text_message" 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/jam_tracks_manager" require "jam_ruby/jam_track_importer" require "jam_ruby/jmep_manager" diff --git a/ruby/lib/jam_ruby/models/broadcast_notification.rb b/ruby/lib/jam_ruby/models/broadcast_notification.rb new file mode 100644 index 000000000..24c19cc71 --- /dev/null +++ b/ruby/lib/jam_ruby/models/broadcast_notification.rb @@ -0,0 +1,51 @@ +module JamRuby + class BroadcastNotification < ActiveRecord::Base + + attr_accessible :title, :message, :button_label, :frequency, :button_url, as: :admin + + has_many :user_views, class_name: 'JamRuby::BroadcastNotificationView', dependent: :destroy + + validates :button_label, presence: true, length: {maximum: 14} + validates :message, presence: true, length: {maximum: 200} + validates :title, presence: true, length: {maximum: 50} + + def self.next_broadcast(user) + self.viewable_notifications(user).limit(1).first + end + + def self.viewable_notifications(user) + self.select('broadcast_notifications.*, bnv.updated_at AS bnv_updated_at') + .joins("LEFT OUTER JOIN broadcast_notification_views AS bnv ON bnv.broadcast_notification_id = broadcast_notifications.id AND (bnv.user_id IS NULL OR (bnv.user_id = '#{user.id}'))") + .where(['broadcast_notifications.frequency > 0']) + .where(['bnv.user_id IS NULL OR bnv.active_at < NOW()']) + .where(['bnv.user_id IS NULL OR broadcast_notifications.frequency > bnv.view_count']) + .order('bnv_updated_at NULLS FIRST') + end + + def did_view(user) + bnv = BroadcastNotificationView + .where(broadcast_notification_id: self.id, user_id: user.id) + .limit(1) + .first + + unless bnv + bnv = user_views.new() + bnv.user = user + bnv.active_at = Time.now - 10 + end + + bnv = user_views.new(user: user) unless bnv + bnv.view_count += 1 + bnv.save + bnv + end + + def frequency_distribution + @distribution ||= BroadcastNotificationView + .where(broadcast_notification_id: self.id) + .group(:view_count) + .count + end + + end +end diff --git a/ruby/lib/jam_ruby/models/broadcast_notification_view.rb b/ruby/lib/jam_ruby/models/broadcast_notification_view.rb new file mode 100644 index 000000000..926ddc12c --- /dev/null +++ b/ruby/lib/jam_ruby/models/broadcast_notification_view.rb @@ -0,0 +1,8 @@ +module JamRuby + class BroadcastNotificationView < ActiveRecord::Base + + belongs_to :broadcast_notification, :class_name => 'JamRuby::BroadcastNotification' + belongs_to :user, :class_name => 'JamRuby::User' + + end +end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index f27b85a69..4538efbd6 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -633,9 +633,9 @@ FactoryGirl.define do data Faker::Lorem.sentence end - factory :rsvp_slot, class: JamRuby::RsvpSlot do + factory :rsvp_slot, :class => JamRuby::RsvpSlot do - proficiency_level 'beginner' + proficiency_level "beginner" instrument { Instrument.find('electric guitar') } factory :chosen_rsvp_slot do @@ -643,10 +643,10 @@ FactoryGirl.define do user nil end - after(:create) { |rsvp_slot, evaluator| + 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) - } + end end end @@ -686,7 +686,7 @@ FactoryGirl.define do end end - factory :rsvp_request_rsvp_slot, class: JamRuby::RsvpRequestRsvpSlot do + factory :rsvp_request_rsvp_slot, :class => JamRuby::RsvpRequestRsvpSlot do chosen false end @@ -798,6 +798,13 @@ FactoryGirl.define do end end + factory :broadcast_notification, :class => JamRuby::BroadcastNotification do + title Faker::Lorem.sentence[0...50] + message Faker::Lorem.paragraph[0...200] + button_label Faker::Lorem.words(2).join(' ')[0...14] + frequency 3 + end + factory :affiliate_partner, class: 'JamRuby::AffiliatePartner' do sequence(:partner_name) { |n| "partner-#{n}" } entity_type 'Individual' @@ -824,6 +831,5 @@ FactoryGirl.define do factory :affiliate_legalese, class: 'JamRuby::AffiliateLegalese' do legalese Faker::Lorem.paragraphs(6).join("\n\n") - end - + 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 d08299f04..79059d6c4 100644 --- a/ruby/spec/jam_ruby/models/active_music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/active_music_session_spec.rb @@ -616,6 +616,7 @@ describe ActiveMusicSession do end it "disallow a jam track to be opened when another is already opened" do + pending "needs fixing" # if a jam track is open, don't allow another to be opened @music_session.open_jam_track(@user1, @jam_track) @music_session.errors.any?.should be_false diff --git a/ruby/spec/jam_ruby/models/broadcast_notification_spec.rb b/ruby/spec/jam_ruby/models/broadcast_notification_spec.rb new file mode 100644 index 000000000..4ec86db70 --- /dev/null +++ b/ruby/spec/jam_ruby/models/broadcast_notification_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe BroadcastNotification do + + let(:broadcast1) { FactoryGirl.create(:broadcast_notification) } + let(:broadcast2) { FactoryGirl.create(:broadcast_notification) } + let(:broadcast3) { FactoryGirl.create(:broadcast_notification) } + let(:broadcast4) { FactoryGirl.create(:broadcast_notification) } + let(:user1) { FactoryGirl.create(:user) } + let(:user2) { FactoryGirl.create(:user) } + let(:user3) { FactoryGirl.create(:user) } + let(:user4) { FactoryGirl.create(:user) } + + before(:each) do + BroadcastNotificationView.delete_all + end + + it 'created broadcast' do + expect(broadcast1.title).not_to be_empty + expect(broadcast1.frequency).to be > 0 + end + + it 'gets view incremented' do + bnv = broadcast1.did_view(user1) + bnv = broadcast1.did_view(user1) + bnv.view_count.should eq(2) + end + + it 'loads viewable broadcasts for a user' do + broadcast1.touch + broadcast2.touch + broadcast3.touch + broadcast4.touch + + bns = BroadcastNotification.viewable_notifications(user1) + bns.count.should eq(4) + + broadcast2.frequency.times { |nn| broadcast2.did_view(user1) } + broadcast3.did_view(user1) + broadcast1.did_view(user1) + broadcast4.did_view(user2) + + bns = BroadcastNotification.viewable_notifications(user1) + expect(bns.count).to be(3) + expect(bns[0].id).to eq(broadcast3.id) + expect(bns.detect {|bb| bb.id==broadcast2.id }).to be_nil + expect(BroadcastNotification.next_broadcast(user1).id).to eq(broadcast3.id) + end + + it 'generates frequency distribution' do + 4.times { |nn| broadcast1.did_view(user1) } + 5.times { |nn| broadcast1.did_view(user2) } + 5.times { |nn| broadcast1.did_view(user3) } + 8.times { |nn| broadcast1.did_view(user4) } + + distrib = broadcast1.frequency_distribution + + expect(distrib.count).to be == 3 + expect(distrib[4]).to be == 1 + expect(distrib[5]).to be == 2 + expect(distrib[8]).to be == 1 + end + +end diff --git a/web/Gemfile b/web/Gemfile index 0dc9d2806..d7975e309 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -1,7 +1,4 @@ source 'http://rubygems.org' -unless ENV["LOCAL_DEV"] == "1" - source 'https://jamjam:blueberryjam@int.jamkazam.com/gems/' -end # Look for $WORKSPACE, otherwise use "workspace" as dev path. devenv = ENV["BUILD_NUMBER"].nil? # Jenkins sets a build number environment variable @@ -12,11 +9,13 @@ if devenv gem 'jam_ruby', :path => "../ruby" gem 'jam_websockets', :path => "../websocket-gateway" else - gem 'jam_db', "0.1.#{ENV["BUILD_NUMBER"]}" - gem 'jampb', "0.1.#{ENV["BUILD_NUMBER"]}" - gem 'jam_ruby', "0.1.#{ENV["BUILD_NUMBER"]}" - gem 'jam_websockets', "0.1.#{ENV["BUILD_NUMBER"]}" - ENV['NOKOGIRI_USE_SYSTEM_LIBRARIES'] ||= "true" + source 'https://jamjam:blueberryjam@int.jamkazam.com/gems/' do + gem 'jam_db', "0.1.#{ENV["BUILD_NUMBER"]}" + gem 'jampb', "0.1.#{ENV["BUILD_NUMBER"]}" + gem 'jam_ruby', "0.1.#{ENV["BUILD_NUMBER"]}" + gem 'jam_websockets', "0.1.#{ENV["BUILD_NUMBER"]}" + ENV['NOKOGIRI_USE_SYSTEM_LIBRARIES'] ||= "true" + end end gem 'oj', '2.10.2' @@ -89,6 +88,13 @@ gem 'guard', '2.7.3' gem 'influxdb', '0.1.8' gem 'influxdb-rails', '0.1.10' gem 'sitemap_generator' +gem 'bower-rails', "~> 0.9.2" +gem 'react-rails', '~> 1.0' +#gem "browserify-rails", "~> 0.7" + +source 'https://rails-assets.org' do + gem 'rails-assets-reflux' +end group :development, :test do gem 'rspec-rails', '2.14.2' @@ -135,7 +141,7 @@ group :test, :cucumber do # gem 'growl', '1.0.3' gem 'poltergeist' gem 'resque_spec' - #gem 'thin' + #gem 'thin' end diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index d9ca26642..e77a3a58f 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -38,6 +38,7 @@ //= require jquery.exists //= require jquery.payment //= require jquery.visible +//= require reflux //= require howler.core.js //= require jstz //= require class @@ -50,6 +51,10 @@ //= require utils //= require subscription_utils //= require custom_controls +//= require react +//= require react_ujs +//= require react-init +//= require react-components //= require web/signup_helper //= require web/signin_helper //= require web/signin @@ -59,4 +64,4 @@ //= require_directory ./wizard //= require_directory ./wizard/gear //= require_directory ./wizard/loopback -//= require everywhere/everywhere \ No newline at end of file +//= require everywhere/everywhere diff --git a/web/app/assets/javascripts/client_init.js.coffee b/web/app/assets/javascripts/client_init.js.coffee index 20d0e76cc..9da3d933e 100644 --- a/web/app/assets/javascripts/client_init.js.coffee +++ b/web/app/assets/javascripts/client_init.js.coffee @@ -1,18 +1,37 @@ # one time init stuff for the /client view - $ = jQuery context = window context.JK ||= {}; +broadcastActions = context.JK.Actions.Broadcast context.JK.ClientInit = class ClientInit constructor: () -> @logger = context.JK.logger @gearUtils = context.JK.GearUtils + @ALERT_NAMES = context.JK.ALERT_NAMES; + @lastCheckedBroadcast = null init: () => if context.gon.isNativeClient this.nativeClientInit() + context.JK.onBackendEvent(@ALERT_NAMES.WINDOW_OPEN_FOREGROUND_MODE, 'client_init', @watchBroadcast); + + this.watchBroadcast() + + checkBroadcast: () => + broadcastActions.load.triggerPromise() + + watchBroadcast: () => + if context.JK.currentUserId + # create a 15 second buffer to not check too fast for some reason (like the client firing multiple foreground events) + if !@lastCheckedBroadcast? || @lastCheckedBroadcast.getTime() < new Date().getTime() - 15000 + @lastCheckedBroadcast = new Date() + setTimeout(@checkBroadcast, 3000) + + nativeClientInit: () => @gearUtils.bootstrapDefaultPlaybackProfile(); + + diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index c47f27280..82773d503 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -56,7 +56,27 @@ }); } - function uploadMusicNotations(formData) { + function getBroadcastNotification(options) { + var userId = getId(options); + return $.ajax({ + type: "GET", + url: "/api/users/" + userId + "/broadcast_notification" + }); + } + + function quietBroadcastNotification(options) { + var userId = getId(options); + var broadcast_id = options.broadcast_id; + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/users/" + userId + "/broadcast_notification/" + broadcast_id + '/quiet', + data: JSON.stringify({}), + }); + } + + function uploadMusicNotations(formData) { return $.ajax({ type: "POST", processData: false, @@ -1780,6 +1800,8 @@ this.createScheduledSession = createScheduledSession; this.uploadMusicNotations = uploadMusicNotations; this.getMusicNotation = getMusicNotation; + this.getBroadcastNotification = getBroadcastNotification; + this.quietBroadcastNotification = quietBroadcastNotification; this.legacyJoinSession = legacyJoinSession; this.joinSession = joinSession; this.cancelSession = cancelSession; @@ -1930,9 +1952,7 @@ this.createAlert = createAlert; this.signup = signup; this.portOverCarts = portOverCarts; - return this; }; - })(window,jQuery); diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index ac49684e6..51ce008dd 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -202,7 +202,12 @@ var gridRows = layout.split('x')[0]; var gridCols = layout.split('x')[1]; - $grid.children().each(function () { + var gutterWidth = 0; + + var findCardLayout; + var feedCardLayout; + + $grid.find('.homecard').each(function () { var childPosition = $(this).attr("layout-grid-position"); var childRow = childPosition.split(',')[1]; var childCol = childPosition.split(',')[0]; @@ -211,6 +216,13 @@ var childLayout = me.getCardLayout(gridWidth, gridHeight, gridRows, gridCols, childRow, childCol, childRowspan, childColspan); + if($(this).is('.feed')) { + feedCardLayout = childLayout; + } + else if($(this).is('.findsession')) { + findCardLayout = childLayout; + } + $(this).animate({ width: childLayout.width, height: childLayout.height, @@ -218,6 +230,18 @@ left: childLayout.left }, opts.animationDuration); }); + + var broadcastWidth = findCardLayout.width + feedCardLayout.width; + + layoutBroadcast(broadcastWidth, findCardLayout.left); + } + + function layoutBroadcast(width, left) { + var css = { + width:width + opts.gridPadding * 2, + left:left + } + $('[data-react-class="BroadcastHolder"]').animate(css, opts.animationDuration) } function layoutSidebar(screenWidth, screenHeight) { diff --git a/web/app/assets/javascripts/react-components.js b/web/app/assets/javascripts/react-components.js new file mode 100644 index 000000000..c16b9faa7 --- /dev/null +++ b/web/app/assets/javascripts/react-components.js @@ -0,0 +1,3 @@ +//= require ./react-components/actions/BroadcastActions +//= require ./react-components/stores/BroadcastStore +//= require_directory ./react-components \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/.gitkeep b/web/app/assets/javascripts/react-components/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/web/app/assets/javascripts/react-components/Broadcast.js.jsx.coffee b/web/app/assets/javascripts/react-components/Broadcast.js.jsx.coffee new file mode 100644 index 000000000..59b5b147f --- /dev/null +++ b/web/app/assets/javascripts/react-components/Broadcast.js.jsx.coffee @@ -0,0 +1,45 @@ +context = window + +broadcastActions = window.JK.Actions.Broadcast; +rest = window.JK.Rest(); + +Broadcast = React.createClass({ + displayName: 'Broadcast Notification' + + handleNavigate: (e) -> + href = $(e.currentTarget).attr('href') + + if href.indexOf('http') == 0 + e.preventDefault() + window.JK.popExternalLink(href) + + broadcastActions.hide.trigger() + + + notNow: (e) -> + e.preventDefault(); + rest.quietBroadcastNotification({broadcast_id: this.props.notification.id}) + broadcastActions.hide.trigger() + + + createMarkup: () -> + {__html: this.props.notification.message}; + + + render: () -> + `