diff --git a/ruby/.gitignore b/ruby/.gitignore
new file mode 100644
index 000000000..a35fe92d7
--- /dev/null
+++ b/ruby/.gitignore
@@ -0,0 +1,22 @@
+*.gem
+*.rbc
+.bundle
+.config
+.yardoc
+Gemfile.lock
+InstalledFiles
+_yardoc
+coverage
+doc/
+lib/bundler/man
+pkg
+rdoc
+spec/reports
+test/tmp
+test/version_tmp
+tmp
+
+.idea
+*~
+*.swp
+*.iml
diff --git a/ruby/.pg_migrate b/ruby/.pg_migrate
new file mode 100644
index 000000000..e6afc2e53
--- /dev/null
+++ b/ruby/.pg_migrate
@@ -0,0 +1 @@
+up.connopts=dbname:jam_ruby
diff --git a/ruby/.rspec b/ruby/.rspec
new file mode 100644
index 000000000..5f1647637
--- /dev/null
+++ b/ruby/.rspec
@@ -0,0 +1,2 @@
+--color
+--format progress
diff --git a/ruby/.ruby-gemset b/ruby/.ruby-gemset
new file mode 100644
index 000000000..90127ddb7
--- /dev/null
+++ b/ruby/.ruby-gemset
@@ -0,0 +1 @@
+jamruby
diff --git a/ruby/.ruby-version b/ruby/.ruby-version
new file mode 100644
index 000000000..abf2ccea0
--- /dev/null
+++ b/ruby/.ruby-version
@@ -0,0 +1 @@
+ruby-2.0.0-p247
diff --git a/ruby/Gemfile b/ruby/Gemfile
new file mode 100644
index 000000000..0a2f8a710
--- /dev/null
+++ b/ruby/Gemfile
@@ -0,0 +1,45 @@
+#ruby=1.9.3
+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.
+workspace = ENV["WORKSPACE"] || "~/workspace"
+devenv = ENV["BUILD_NUMBER"].nil? # Jenkins sets a build number environment variable
+
+gem 'pg', '0.15.1', :platform => [:mri, :mswin, :mingw]
+gem 'jdbc_postgres', :platform => [:jruby]
+
+gem 'activerecord', '3.2.13'
+gem 'uuidtools', '2.1.2'
+gem 'bcrypt-ruby', '3.0.1'
+gem 'ruby-protocol-buffers', '1.2.2'
+gem 'eventmachine', '1.0.3'
+gem 'amqp', '1.0.2'
+gem 'will_paginate'
+gem 'actionmailer', '3.2.13'
+gem 'sendgrid'
+gem 'aws-sdk', '1.8.0'
+gem 'carrierwave'
+gem 'aasm', '3.0.16'
+gem 'devise', '>= 1.1.2'
+gem 'postgres-copy'
+
+if devenv
+ gem 'jam_db', :path=> "#{workspace}/jam-db/target/ruby_package"
+ gem 'jampb', :path => "#{workspace}/jam-pb/target/ruby/jampb"
+else
+ gem 'jam_db'
+ gem 'jampb'
+end
+
+group :test do
+ gem "factory_girl", '4.1.0'
+ gem "rspec", "2.10.0"
+ gem 'spork', '0.9.0'
+ gem 'database_cleaner', '0.7.0'
+end
+
+# Specify your gem's dependencies in jam_ruby.gemspec
+gemspec
diff --git a/ruby/LICENSE b/ruby/LICENSE
new file mode 100644
index 000000000..d990cbbc0
--- /dev/null
+++ b/ruby/LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2012 Seth Call
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/ruby/README.md b/ruby/README.md
new file mode 100644
index 000000000..39a2c2e30
--- /dev/null
+++ b/ruby/README.md
@@ -0,0 +1,8 @@
+# JamRuby
+
+## Environment
+Create development database 'jam_ruby'
+`createdb jam_ruby`
+
+Once you've created your database, migrate it:
+`bundle exec jam_ruby up`
diff --git a/ruby/Rakefile b/ruby/Rakefile
new file mode 100644
index 000000000..f57ae68a8
--- /dev/null
+++ b/ruby/Rakefile
@@ -0,0 +1,2 @@
+#!/usr/bin/env rake
+require "bundler/gem_tasks"
diff --git a/ruby/bin/mix_cron.rb b/ruby/bin/mix_cron.rb
new file mode 100755
index 000000000..2d106787b
--- /dev/null
+++ b/ruby/bin/mix_cron.rb
@@ -0,0 +1,97 @@
+# Ensure the cron is using the proper version of ruby, and simply run this with:
+# ruby mix_cron.rb
+#
+
+require 'faraday'
+require 'json'
+require 'tempfile'
+
+MIXER_EXECUTABLE = "/usr/local/bin/audiomixerapp"
+S3CMD = "s3cmd"
+
+# FIXME: This should probably come from an environments file or something
+BASE_URL = "http://www.jamkazam.com"
+
+# This must be present on requests from the cron to prevent hackers from
+# hitting these routes.
+CRON_TOKEN = "2kkl39sjjf3ijdsflje2923j"
+
+AUDIOMIXER_LOG_FILE = "/var/log/audiomixer"
+
+MIX_CRON_WATCH_FILE = "/var/run/mix_cron"
+
+
+# Don't do anything if the cron is arleady running. There's a theoretical race
+# condition here, but it should never actually be a problem because the cron should
+# only run every minute or two.
+if File.exist?(MIX_CRON_WATCH_FILE)
+ psret = `ps axuw | grep mix_cron.rb | grep ruby`
+ unless psret.empty?
+ puts "Cron still running"
+ exit
+ end
+end
+
+`touch #{MIX_CRON_WATCH_FILE}`
+
+
+# Get the next manifest to mix
+response = Faraday.get "#{BASE_URL}/mixes/next", :token => CRON_TOKEN
+if response.status > 299
+ puts "Error response getting next mix: #{response.status}, #{response.body}"
+ do_exit
+end
+
+# This just means no mixes available.
+if response.status == 204
+ do_exit
+end
+
+
+if response.status != 200
+ puts "Unexpected response received: #{response.status}, #{response.body}"
+ do_exit
+end
+
+json = JSON.parse(response.body)
+# This needs to download all the vorbis files, mix and then upload
+# the finished one, and tell the server about that.
+json['manifest']['files'].map! do |file|
+ file['filename'] = Dir::Tmpname.make_tmpname ['/tmp/', '.ogg'], nil
+ file_response = Faraday.get file.url
+ if file_response.status != 200
+ puts "Error downloading url: #{file.url}"
+ do_exit
+ end
+ File.open(file['filename'], 'wb') { |fp| fp.write(file_response.body) }
+end
+
+output_filename = "/tmp/mixout-#{json['id']}.ogg"
+IO.popen("#{MIXER_EXECUTABLE} #{output_filename} vorbis >>& #{AUDIOMIXER_LOG_FILE}", "w") do |f|
+ f.puts JSON.generate(json)
+ f.close
+end
+
+# First maybe make sure the length is reasonable or something? I bet sox can check that (duration i mean).
+
+# FIXME?: Need to check that the put succeeded before carrying on. Probably can use the exit code or some such.
+# Or maybe just do an ls to sanity check it.
+`#{S3CMD} -P put #{output_filename} #{json['destination']}`
+
+finish_response = Faraday.put "#{BASE_URL}/mixes/finish", :token => CRON_TOKEN, :id => json['id']
+if finish_response.status != 204
+ puts "Error calling finish on server for mix_id #{json['id']}: #{finish_response.status}, #{finish_response.body}"
+ do_exit
+end
+
+puts "Mix complete and uploaded to: #{json['destination']}"
+do_exit
+
+
+
+
+def do_exit
+ `rm -f #{MIX_CRON_WATCH_FILE}`
+end
+
+
diff --git a/ruby/build b/ruby/build
new file mode 100755
index 000000000..df9ac25e3
--- /dev/null
+++ b/ruby/build
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+echo "updating dependencies"
+bundle install --path vendor/bundle --local
+bundle update
+
+echo "running rspec tests"
+bundle exec rspec
+
+if [ "$?" = "0" ]; then
+ echo "tests completed"
+else
+ echo "tests failed."
+ exit 1
+fi
+
+echo "build complete"
+
diff --git a/ruby/config/aws.yml b/ruby/config/aws.yml
new file mode 100644
index 000000000..6fc2c8fc4
--- /dev/null
+++ b/ruby/config/aws.yml
@@ -0,0 +1,11 @@
+development:
+ access_key_id: AKIAIFFBNBRQG5YQ5WHA
+ secret_access_key: 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
+
+test:
+ access_key_id: AKIAIFFBNBRQG5YQ5WHA
+ secret_access_key: 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
+
+production:
+ access_key_id: AKIAIFFBNBRQG5YQ5WHA
+ secret_access_key: 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
diff --git a/ruby/config/database.yml b/ruby/config/database.yml
new file mode 100644
index 000000000..77c6e0e33
--- /dev/null
+++ b/ruby/config/database.yml
@@ -0,0 +1,9 @@
+test:
+ adapter: postgresql
+ database: jam_ruby_test
+ host: localhost
+ pool: 3
+ username: postgres
+ password: postgres
+ timeout: 2000
+ encoding: unicode
diff --git a/ruby/config/profanity.yml b/ruby/config/profanity.yml
new file mode 100644
index 000000000..35767f0e0
--- /dev/null
+++ b/ruby/config/profanity.yml
@@ -0,0 +1,202 @@
+# Note: I got this from here: https://github.com/intridea/profanity_filter/blob/master/config/dictionary.yml
+# I doubt that this list can be copyrighted
+# the filter currently checks for words that are 3 or more characters.
+---
+ass: "*ss"
+asses: "*ss*s"
+asshole: "*ssh*l*"
+assholes: "*ssh*l*s"
+bastard: b*st*rd
+beastial: b**st**l
+beastiality: b**st**l*ty
+beastility: b**st*l*ty
+bestial: b*st**l
+bestiality: b*st**l*ty
+bitch: b*tch
+bitcher: b*tch*r
+bitchers: b*tch*rs
+bitches: b*tch*s
+bitchin: b*tch*n
+bitching: b*tch*ng
+blowjob: bl*wj*b
+blowjobs: bl*wj*bs
+bullshit: b*llsh*t
+clit: cl*t
+cock: c*ck
+cocks: c*cks
+cocksuck: c*cks*ck
+cocksucked: c*cks*ck*d
+cocksucker: c*cks*ck*r
+cocksucking: c*cks*ck*ng
+cocksucks: c*cks*cks
+cum: c*m
+cummer: c*mm*r
+cumming: c*mm*ng
+cums: c*ms
+cumshot: c*msh*t
+cunillingus: c*n*ll*ng*s
+cunnilingus: c*nn*l*ng*s
+cunt: c*nt
+cuntlick: c*ntl*ck
+cuntlicker: c*ntl*ck*r
+cuntlicking: c*ntl*ck*ng
+cunts: c*nts
+cyberfuc: cyb*rf*c
+cyberfuck: cyb*rf*ck
+cyberfucked: cyb*rf*ck*d
+cyberfucker: cyb*rf*ck*r
+cyberfuckers: cyb*rf*ck*rs
+cyberfucking: cyb*rf*ck*ng
+damn: d*mn
+dildo: d*ld*
+dildos: d*ld*s
+dick: d*ck
+dink: d*nk
+dinks: d*nks
+ejaculate: "*j*c*l*t*"
+ejaculated: "*j*c*l*t*d"
+ejaculates: "*j*c*l*t*s"
+ejaculating: "*j*c*l*t*ng"
+ejaculatings: "*j*c*l*t*ngs"
+ejaculation: "*j*c*l*t**n"
+fag: f*g
+fagging: f*gg*ng
+faggot: f*gg*t
+faggs: f*ggs
+fagot: f*g*t
+fagots: f*g*ts
+fags: f*gs
+fart: f*rt
+farted: f*rt*d
+farting: f*rt*ng
+fartings: f*rt*ngs
+farts: f*rts
+farty: f*rty
+felatio: f*l*t**
+fellatio: f*ll*t**
+fingerfuck: f*ng*rf*ck
+fingerfucked: f*ng*rf*ck*d
+fingerfucker: f*ng*rf*ck*r
+fingerfuckers: f*ng*rf*ck*rs
+fingerfucking: f*ng*rf*ck*ng
+fingerfucks: f*ng*rf*cks
+fistfuck: f*stf*ck
+fistfucked: f*stf*ck*d
+fistfucker: f*stf*ck*r
+fistfuckers: f*stf*ck*rs
+fistfucking: f*stf*ck*ng
+fistfuckings: f*stf*ck*ngs
+fistfucks: f*stf*cks
+fuck: f*ck
+fucked: f*ck*d
+fucker: f*ck*r
+fuckers: f*ck*rs
+fuckin: f*ck*n
+fucking: f*ck*ng
+fuckings: f*ck*ngs
+fuckme: f*ckm*
+fucks: f*cks
+fuk: f*k
+fuks: f*ks
+gangbang: g*ngb*ng
+gangbanged: g*ngb*ng*d
+gangbangs: g*ngb*ngs
+gaysex: g*ys*x
+goddamn: g*dd*mn
+hardcoresex: h*rdc*r*s*x
+hell: h*ll
+horniest: h*rn**st
+horny: h*rny
+hotsex: h*ts*x
+jism: j*sm
+jiz: j*z
+jizm: j*zm
+kock: k*ck
+kondum: k*nd*m
+kondums: k*nd*ms
+kum: k*m
+kumer: k*mm*r
+kummer: k*mm*r
+kumming: k*mm*ng
+kums: k*ms
+kunilingus: k*n*l*ng*s
+lust: l*st
+lusting: l*st*ng
+mothafuck: m*th*f*ck
+mothafucka: m*th*f*ck*
+mothafuckas: m*th*f*ck*s
+mothafuckaz: m*th*f*ck*z
+mothafucked: m*th*f*ck*d
+mothafucker: m*th*f*ck*r
+mothafuckers: m*th*f*ck*rs
+mothafuckin: m*th*f*ck*n
+mothafucking: m*th*f*ck*ng
+mothafuckings: m*th*f*ck*ngs
+mothafucks: m*th*f*cks
+motherfuck: m*th*rf*ck
+motherfucked: m*th*rf*ck*d
+motherfucker: m*th*rf*ck*r
+motherfuckers: m*th*rf*ck*rs
+motherfuckin: m*th*rf*ck*n
+motherfucking: m*th*rf*ck*ng
+motherfuckings: m*th*rf*ck*ngs
+motherfucks: m*th*rf*cks
+niger: n*gg*r
+nigger: n*gg*r
+niggers: n*gg*rs
+orgasim: "*rg*s*m"
+orgasims: "*rg*s*ms"
+orgasm: "*rg*sm"
+orgasms: "*rg*sms"
+phonesex: ph*n*s*x
+phuk: ph*k
+phuked: ph*k*d
+phuking: ph*k*ng
+phukked: ph*kk*d
+phukking: ph*kk*ng
+phuks: ph*ks
+phuq: ph*q
+pis: p*ss
+piss: p*ss
+pisser: p*ss*r
+pissed: p*ss*d
+pisser: p*ss*r
+pissers: p*ss*rs
+pises: p*ss*s
+pisses: p*ss*s
+pisin: p*ss*n
+pissin: p*ss*n
+pising: p*ss*ng
+pissing: p*ss*ng
+pisof: p*ss*ff
+pissoff: p*ss*ff
+porn: p*rn
+porno: p*rn*
+pornography: p*rn*gr*phy
+pornos: p*rn*s
+prick: pr*ck
+pricks: pr*cks
+pussies: p*ss**s
+pusies: p*ss**s
+pussy: p*ssy
+pusy: p*ssy
+pussys: p*ssys
+pusys: p*ssys
+shit: sh*t
+shited: sh*t*d
+shitfull: sh*tf*ll
+shiting: sh*t*ng
+shitings: sh*t*ngs
+shits: sh*ts
+shitted: sh*tt*d
+shitter: sh*tt*r
+shitters: sh*tt*rs
+shitting: sh*tt*ng
+shittings: sh*tt*ngs
+shitty: sh*tty
+shity: sh*tty
+slut: sl*t
+sluts: sl*ts
+smut: sm*t
+spunk: sp*nk
+twat: tw*t
diff --git a/ruby/jam_ruby.gemspec b/ruby/jam_ruby.gemspec
new file mode 100644
index 000000000..ccb082418
--- /dev/null
+++ b/ruby/jam_ruby.gemspec
@@ -0,0 +1,17 @@
+# -*- encoding: utf-8 -*-
+require File.expand_path('../lib/jam_ruby/version', __FILE__)
+
+Gem::Specification.new do |gem|
+ gem.authors = ["Seth Call"]
+ gem.email = ["seth@jamkazam.com"]
+ gem.description = %q{Common library for JamKazam Ruby code}
+ gem.summary = %q{Common library for JamKazam Ruby code}
+ gem.homepage = ""
+
+ gem.files = `git ls-files`.split($\)
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
+ gem.name = "jam_ruby"
+ gem.require_paths = ["lib"]
+ gem.version = JamRuby::VERSION
+end
diff --git a/ruby/jenkins b/ruby/jenkins
new file mode 100755
index 000000000..04afb0992
--- /dev/null
+++ b/ruby/jenkins
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+GEM_SERVER=http://localhost:9000/gems
+
+echo "starting build..."
+./build
+
+if [ "$?" = "0" ]; then
+ echo "build succeeded"
+
+ # generate gem version based on jenkins build number
+ if [ -z $BUILD_NUMBER ]; then
+ BUILD_NUMBER="1"
+ fi
+ VERSION="0.0.${BUILD_NUMBER}"
+ echo "packaging gem jam_ruby-$VERSION"
+ cat > lib/jam_ruby/version.rb << EOF
+module JamRuby
+ VERSION = "$VERSION"
+end
+EOF
+
+ gem build jam_ruby.gemspec
+
+ GEMNAME="jam_ruby-${VERSION}.gem"
+
+ echo "publishing gem"
+ curl -f -T $GEMNAME $GEM_SERVER/$GEMNAME
+
+ if [ "$?" != "0" ]; then
+ echo "publish failed"
+ exit 1
+ fi
+ echo "done publishing gems"
+else
+ echo "build failed"
+ exit 1
+fi
+
+
diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb
new file mode 100755
index 000000000..af074e607
--- /dev/null
+++ b/ruby/lib/jam_ruby.rb
@@ -0,0 +1,83 @@
+require "pg"
+require "active_record"
+require "carrierwave"
+require "carrierwave/orm/activerecord"
+require "jampb"
+require "uuidtools"
+require "logging"
+require "will_paginate"
+require "will_paginate/active_record"
+require "action_mailer"
+require "devise"
+require "sendgrid"
+require 'postgres-copy'
+require "jam_ruby/constants/limits"
+require "jam_ruby/constants/notification_types"
+require "jam_ruby/constants/validation_messages"
+require "jam_ruby/errors/permission_error"
+require "jam_ruby/errors/state_error"
+require "jam_ruby/errors/jam_argument_error"
+require "jam_ruby/mq_router"
+require "jam_ruby/base_manager"
+require "jam_ruby/connection_manager"
+require "jam_ruby/version"
+require "jam_ruby/environment"
+require "jam_ruby/init"
+require "jam_ruby/app/mailers/user_mailer"
+require "jam_ruby/app/mailers/invited_user_mailer"
+require "jam_ruby/app/mailers/corp_mailer"
+require "jam_ruby/app/uploaders/artifact_uploader"
+require "jam_ruby/app/uploaders/perf_data_uploader"
+require "jam_ruby/lib/desk_multipass"
+require "jam_ruby/lib/s3_util"
+require "jam_ruby/lib/s3_manager"
+require "jam_ruby/lib/profanity"
+require "jam_ruby/amqp/amqp_connection_manager"
+require "jam_ruby/message_factory"
+require "jam_ruby/models/feedback"
+require "jam_ruby/models/feedback_observer"
+require "jam_ruby/models/max_mind_geo"
+require "jam_ruby/models/max_mind_isp"
+require "jam_ruby/models/genre"
+require "jam_ruby/models/user"
+require "jam_ruby/models/user_observer"
+require "jam_ruby/models/user_authorization"
+require "jam_ruby/models/join_request"
+require "jam_ruby/models/band"
+require "jam_ruby/models/invited_user"
+require "jam_ruby/models/invited_user_observer"
+require "jam_ruby/models/artifact_update"
+require "jam_ruby/models/band_invitation"
+require "jam_ruby/models/band_liker"
+require "jam_ruby/models/band_follower"
+require "jam_ruby/models/band_following"
+require "jam_ruby/models/band_musician"
+require "jam_ruby/models/connection"
+require "jam_ruby/models/friendship"
+require "jam_ruby/models/music_session"
+require "jam_ruby/models/music_session_history"
+require "jam_ruby/models/music_session_user_history"
+require "jam_ruby/models/music_session_perf_data"
+require "jam_ruby/models/invitation"
+require "jam_ruby/models/fan_invitation"
+require "jam_ruby/models/friend_request"
+require "jam_ruby/models/instrument"
+require "jam_ruby/models/musician_instrument"
+require "jam_ruby/models/notification"
+require "jam_ruby/models/track"
+require "jam_ruby/models/user_liker"
+require "jam_ruby/models/user_like"
+require "jam_ruby/models/user_follower"
+require "jam_ruby/models/user_following"
+require "jam_ruby/models/search"
+require "jam_ruby/models/recording"
+require "jam_ruby/models/recorded_track"
+require "jam_ruby/models/mix"
+require "jam_ruby/models/claimed_recording"
+require "jam_ruby/models/crash_dump"
+
+include Jampb
+
+module JamRuby
+
+end
diff --git a/ruby/lib/jam_ruby/amqp/amqp_connection_manager.rb b/ruby/lib/jam_ruby/amqp/amqp_connection_manager.rb
new file mode 100644
index 000000000..644449b64
--- /dev/null
+++ b/ruby/lib/jam_ruby/amqp/amqp_connection_manager.rb
@@ -0,0 +1,101 @@
+module JamRuby
+ # The purpose of this class is to handle reconnect logic and 'recover' logic (which means automatically resubscribe to topics/queues).
+ # It's 'leaky' in that it will give you a AMQP::Channel to do these subscriptions yourself in the block you pass to connect.
+ # Use the *connected* property to check if the connection is currently up.
+ class AmqpConnectionManager
+
+ attr_accessor :should_reconnect, :reconnect_interval, :connection, :connect, :connect_options, :connect_block, :channel,
+ :connected
+
+ def initialize(should_reconnect, reconnect_interval, connect_options = {})
+ @should_reconnect = should_reconnect
+ @reconnect_interval = reconnect_interval
+ @connect_options = connect_options
+ @connected = false
+ @log = Logging.logger[self]
+ end
+
+ # the block you pass in will be passed a channel upon successful connect. You need
+ #
+ def connect(&block)
+ @connect = true # indicate that we should be connected
+ @connect_block = block
+
+ try_connect
+ end
+
+ def try_connect
+ @connection = AMQP.connect(@connect_options, &method(:successful_connect))
+ @connection.on_tcp_connection_failure(&method(:on_tcp_connection_failure))
+ @connection.on_tcp_connection_loss(&method(:on_tcp_connection_loss))
+ @connection.on_recovery(&method(:on_recovery))
+ @connection.on_error(&method(:on_error))
+ end
+
+ def successful_connect(connection)
+ @log.debug "connected to #{@connect_options}"
+ @connected = true
+
+ @channel = AMQP::Channel.new(connection)
+ @channel.auto_recovery = true
+
+ unless @connect_block.nil?
+ @connect_block.call(@channel)
+ end
+ end
+
+ def on_tcp_connection_failure(settings)
+ @connected = false
+
+ if @connect && @should_reconnect
+ @log.warn "[network failure] Trying to connect in 4 seconds to #{@connect_options}"
+ EventMachine.add_timer(@reconnect_interval, &method(:try_connect))
+ end
+ end
+
+ def on_tcp_connection_loss(conn, settings)
+ @connected = false
+ if @connect && @should_reconnect
+ @log.warn "[network failure] Trying to reconnect..."
+ conn.reconnect(false, @reconnect_interval)
+ end
+ end
+
+ def on_recovery(conn, settings)
+ @connected = true
+
+ @log.debug "reconnected #{conn} #{settings}"
+
+ #puts "#channel before #{@channel}"
+ #puts "recovered channel: #{@channel.reuse}"
+ end
+
+ def disconnect
+ @connect = false # indicate that we should no longer be connected
+
+ unless @connection.nil?
+ if @connection.connected?
+ @connection.disconnect do
+ @connected = false
+ @log.debug "disconnected"
+ end
+ end
+ end
+ end
+
+ def on_error(connection, connection_close)
+ @log.error "Handling a connection-level exception."
+
+ @log.error "AMQP class id : #{connection_close.class_id}"
+ @log.error "AMQP method id: #{connection_close.method_id}"
+ @log.error "Status code : #{connection_close.reply_code}"
+ @log.error "Error message : #{connection_close.reply_text}"
+ end
+
+ def connected?
+ return @connected
+ end
+ end
+
+
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/mailers/corp_mailer.rb b/ruby/lib/jam_ruby/app/mailers/corp_mailer.rb
new file mode 100644
index 000000000..0bdd09858
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/mailers/corp_mailer.rb
@@ -0,0 +1,35 @@
+module JamRuby
+ # CorpMail 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 CorpMailer < ActionMailer::Base
+ include SendGrid
+
+ layout "user_mailer"
+
+ DEFAULT_SENDER = "noreply@jamkazam.com"
+
+ default :from => DEFAULT_SENDER
+
+ sendgrid_category :use_subject_lines
+ #sendgrid_enable :opentrack, :clicktrack # this makes our emails creepy, imo (seth)
+ sendgrid_unique_args :env => Environment.mode
+
+ def feedback(feedback)
+ @email = feedback.email
+ @body = feedback.body
+
+ sendgrid_category "Corporate"
+ sendgrid_unique_args :type => "feedback"
+
+ mail(:to => "info@jamkazam.com", :subject => "Feedback received from #{@email} ") do |format|
+ format.text
+ format.html
+ end
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/app/mailers/invited_user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/invited_user_mailer.rb
new file mode 100644
index 000000000..a443f9c6d
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/mailers/invited_user_mailer.rb
@@ -0,0 +1,54 @@
+module JamRuby
+ # InvitedUserMailer 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 InvitedUserMailer were to be used in another project, it would need to be configured there, as well.
+
+ # Templates for InvitedUserMailer can be found in jam_ruby/app/views/jam_ruby/user_mailer
+ class InvitedUserMailer < ActionMailer::Base
+ include SendGrid
+
+ DEFAULT_SENDER = "support@jamkazam.com"
+
+ 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
+
+ # sent in the case of a general 'service invitation', from no one person in particular
+ def welcome_betauser(invited_user)
+
+ @signup_url = generate_signup_url(invited_user)
+ @suppress_user_has_account_footer = true
+
+ sendgrid_category "Welcome"
+ sendgrid_unique_args :type => "welcome_betauser"
+
+ mail(:to => invited_user.email, :subject => "Welcome to the JamKazam Beta release") do |format|
+ format.text
+ format.html { render :layout => "user_mailer" }
+ end
+ end
+
+ # used when sending an invitation from one user to another
+ def friend_invitation(invited_user)
+ @signup_url = generate_signup_url(invited_user)
+ @sender = invited_user.sender
+ @note = invited_user.note
+ @suppress_user_has_account_footer = true
+ sendgrid_category "Invitation"
+ sendgrid_unique_args :type => "friend_invitation"
+
+ mail(:to => invited_user.email, :subject => "You've been invited to JamKazam by #{invited_user.sender.name}") do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def generate_signup_url(invited_user)
+ "http://www.jamkazam.com/signup?invitation_code=#{invited_user.invitation_code}"
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb
new file mode 100644
index 000000000..ff5740718
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb
@@ -0,0 +1,71 @@
+ 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 "user_mailer"
+
+ DEFAULT_SENDER = "support@jamkazam.com"
+
+ default :from => DEFAULT_SENDER
+
+ sendgrid_category :use_subject_lines
+ #sendgrid_enable :opentrack, :clicktrack # this makes our emails creepy, imo (seth)
+ sendgrid_unique_args :env => Environment.mode
+
+ def welcome_message(user, signup_confirm_url)
+ @user = user
+ @signup_confirm_url = signup_confirm_url
+ sendgrid_category "Welcome"
+ sendgrid_unique_args :type => "welcome_message"
+
+ mail(:to => user.email, :subject => "Welcome to JamKazam, #{user.first_name} ") do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def password_changed(user)
+ @user = user
+ 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
+ @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_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_unique_args :type => "updated_email"
+ mail(:to => user.email, :subject => "JamKazam Email Changed") do |format|
+ format.text
+ format.html
+ 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
new file mode 100644
index 000000000..89dc991e1
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/uploaders/artifact_uploader.rb
@@ -0,0 +1,59 @@
+# encoding: utf-8
+
+
+ class ArtifactUploader < CarrierWave::Uploader::Base
+ # Include RMagick or MiniMagick support:
+ # include CarrierWave::RMagick
+ # include CarrierWave::MiniMagick
+
+ # Include the Sprockets helpers for Rails 3.1+ asset pipeline compatibility:
+ # include Sprockets::Helpers::RailsHelper
+ # include Sprockets::Helpers::IsolatedHelper
+
+ # Choose what kind of storage to use for this uploader:
+ # storage :file
+ # storage :fog
+
+ # Override the directory where uploaded files will be stored.
+ # This is a sensible default for uploaders that are meant to be mounted:
+ def store_dir
+ "artifacts/#{model.product}/#{model.version}"
+ end
+
+ def md5
+ @md5 ||= ::Digest::MD5.file(current_path).hexdigest
+ end
+
+ # Provide a default URL as a default if there hasn't been a file uploaded:
+ # def default_url
+ # # For Rails 3.1+ asset pipeline compatibility:
+ # # asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
+ #
+ # "/images/fallback/" + [version_name, "default.png"].compact.join('_')
+ # end
+
+ # Process files as they are uploaded:
+ # process :scale => [200, 300]
+ #
+ # def scale(width, height)
+ # # do something
+ # end
+
+ # Create different versions of your uploaded files:
+ # version :thumb do
+ # process :scale => [50, 50]
+ # end
+
+ # 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)
+ end
+
+ # Override the filename of the uploaded files:
+ # Avoid using model.id or version_name here, see uploader/store.rb for details.
+ # def filename
+ # "something.jpg" if original_filename
+ # end
+
+end
diff --git a/ruby/lib/jam_ruby/app/uploaders/perf_data_uploader.rb b/ruby/lib/jam_ruby/app/uploaders/perf_data_uploader.rb
new file mode 100644
index 000000000..fbcbb5a40
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/uploaders/perf_data_uploader.rb
@@ -0,0 +1,59 @@
+# encoding: utf-8
+
+class PerfDataUploader < CarrierWave::Uploader::Base
+
+ # Include RMagick or MiniMagick support:
+ # include CarrierWave::RMagick
+ # include CarrierWave::MiniMagick
+
+ # Include the Sprockets helpers for Rails 3.1+ asset pipeline compatibility:
+ # include Sprockets::Helpers::RailsHelper
+ # include Sprockets::Helpers::IsolatedHelper
+
+ # Choose what kind of storage to use for this uploader:
+ # storage :file
+ # storage :fog
+
+ # Override the directory where uploaded files will be stored.
+ # This is a sensible default for uploaders that are meant to be mounted:
+ def store_dir
+ "perf_data/#{model.id}/#{model.client_id}-#{model.created_at}"
+ end
+
+ def md5
+ @md5 ||= ::Digest::MD5.file(current_path).hexdigest
+ end
+
+ # Provide a default URL as a default if there hasn't been a file uploaded:
+ # def default_url
+ # # For Rails 3.1+ asset pipeline compatibility:
+ # # asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
+ #
+ # "/images/fallback/" + [version_name, "default.png"].compact.join('_')
+ # end
+
+ # Process files as they are uploaded:
+ # process :scale => [200, 300]
+ #
+ # def scale(width, height)
+ # # do something
+ # end
+
+ # Create different versions of your uploaded files:
+ # version :thumb do
+ # process :scale => [50, 50]
+ # end
+
+ # 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)
+ end
+
+ # Override the filename of the uploaded files:
+ # Avoid using model.id or version_name here, see uploader/store.rb for details.
+ # def filename
+ # "something.jpg" if original_filename
+ # end
+
+end
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/corp_mailer/feedback.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/corp_mailer/feedback.html.erb
new file mode 100644
index 000000000..7daab2af6
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/corp_mailer/feedback.html.erb
@@ -0,0 +1,14 @@
+
+
+Feedback Received
+From <%= @email %>:
+<%= @body %>
+
+
+
+
+
+This email was received because someone left feedback at http://www.jamkazam.com/corp/contact
+
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/corp_mailer/feedback.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/corp_mailer/feedback.text.erb
new file mode 100644
index 000000000..4612cf5ee
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/corp_mailer/feedback.text.erb
@@ -0,0 +1,8 @@
+Feedback Received
+
+From <%= @email %>:
+
+<%= @body %>
+
+
+This email was received because someone left feedback at http://www.jamkazam.com/corp/contact
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/invited_user_mailer/friend_invitation.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/invited_user_mailer/friend_invitation.html.erb
new file mode 100644
index 000000000..11c4bffd7
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/invited_user_mailer/friend_invitation.html.erb
@@ -0,0 +1,12 @@
+<% provide(:title, "You've been invited to JamKazam by #{@sender.name}!") %>
+<% provide(:photo_url, @sender.resolved_photo_url) %>
+
+To signup, please go to the create account page.
+
+<% content_for :note do %>
+ <% if @note %>
+ <%= @sender.name %> says: <%= @note %>
+ <% else %>
+ <%= @sender.name %> would like you to join JamKazam.
+ <% end %>
+<% end %>
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/invited_user_mailer/friend_invitation.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/invited_user_mailer/friend_invitation.text.erb
new file mode 100644
index 000000000..5bfbd9b17
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/invited_user_mailer/friend_invitation.text.erb
@@ -0,0 +1,7 @@
+You've been invited to JamKazam by <%= @sender.name %>!
+<% unless @note.nil? %>
+<%= @sender.name %> says:
+<%= @note %>
+<% end %>
+
+To signup, please go to the 'create account' page: <%= @signup_url %>.
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/invited_user_mailer/welcome_betauser.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/invited_user_mailer/welcome_betauser.html.erb
new file mode 100644
index 000000000..c1aafeaee
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/invited_user_mailer/welcome_betauser.html.erb
@@ -0,0 +1,3 @@
+<% provide(:title, 'Welcome to the JamKazam Beta!') %>
+
+To signup, please go to the create account page.
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/invited_user_mailer/welcome_betauser.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/invited_user_mailer/welcome_betauser.text.erb
new file mode 100644
index 000000000..24702c702
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/invited_user_mailer/welcome_betauser.text.erb
@@ -0,0 +1,2 @@
+Welcome to the JamKazam Beta!
+To signup, please go to the 'create account' page: <%= @signup_url %>.
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/password_changed.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/password_changed.html.erb
new file mode 100644
index 000000000..a140d13fe
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/password_changed.html.erb
@@ -0,0 +1,3 @@
+<% provide(:title, 'Jamkazam Password Changed') %>
+
+You just changed your password at Jamkazam.
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/password_changed.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/password_changed.text.erb
new file mode 100644
index 000000000..7dec8f658
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/password_changed.text.erb
@@ -0,0 +1 @@
+You just changed your password!
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/password_reset.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/password_reset.html.erb
new file mode 100644
index 000000000..06bd2dcb2
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/password_reset.html.erb
@@ -0,0 +1,3 @@
+<% provide(:title, 'Jamkazam Password Reset') %>
+
+Visit this link so that you can change your Jamkazam password: reset password.
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/password_reset.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/password_reset.text.erb
new file mode 100644
index 000000000..f0f0e7502
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/password_reset.text.erb
@@ -0,0 +1 @@
+Visit this link so that you can change your Jamkazam password: Reset Password.
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/updated_email.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/updated_email.html.erb
new file mode 100644
index 000000000..4ca9c6e5d
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/updated_email.html.erb
@@ -0,0 +1,3 @@
+<% provide(:title, 'Jamkazam Email Confirmed') %>
+
+<%= @user.email %> has been confirmed as your new email address.
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/updated_email.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/updated_email.text.erb
new file mode 100644
index 000000000..dae13d028
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/updated_email.text.erb
@@ -0,0 +1 @@
+<%= @user.email %> has been confirmed as your new email address.
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/updating_email.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/updating_email.html.erb
new file mode 100644
index 000000000..c6ecd5a1f
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/updating_email.html.erb
@@ -0,0 +1,3 @@
+<% provide(:title, 'Please Confirm New Jamkazam Email') %>
+
+Please click the following link to confirm your change in email: confirm email.
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/updating_email.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/updating_email.text.erb
new file mode 100644
index 000000000..13f92ba7a
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/updating_email.text.erb
@@ -0,0 +1 @@
+Please click the following link to confirm your change in email: <%= @user.update_email_confirmation_url %>.
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
new file mode 100644
index 000000000..0358cb78d
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb
@@ -0,0 +1,4 @@
+<% provide(:title, 'Welcome to Jamkazam') %>
+
+Welcome to Jamkazam, <%= @user.first_name %>!
+To confirm this email address, please go to the signup confirmation page..
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
new file mode 100644
index 000000000..73ad001bd
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb
@@ -0,0 +1,3 @@
+Welcome to Jamkazam, <%= @user.first_name %>!
+
+To confirm this email address, please go to the signup confirmation page at: <%= @signup_confirm_url %>.
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/layouts/README.md b/ruby/lib/jam_ruby/app/views/layouts/README.md
new file mode 100644
index 000000000..f24c4abc7
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/layouts/README.md
@@ -0,0 +1,15 @@
+Understanding Jamkazam Email Layouts
+====================================
+
+We have two types of layouts;
+* a 'standard' email layout which is an email to a user (user_mailer.html.erb/text)
+* a 'from a user to another user' email layout (from_user_mailer.html.erb/text)
+
+## user_mailer.html.erb
+To use the user_mailer.html.erb layout, you must provide a title section, as well as the body.
+Look at 'password_changed.html.erb' for an example.
+
+## from_user_mailer.html.erb
+To use the from_user_mailer.html.erb layout, you must provide a title section, photo_url section (photo of the person who sent the email), and a note section (any personalized note that the sender may have said, or boilerplate)
+Look at 'friend_invitation.html.erb' for an example.
+
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
new file mode 100644
index 000000000..50da8c408
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.html.erb
@@ -0,0 +1,67 @@
+
+
+
+
+ JamKazam
+
+
+
+
+
+
+
+
+  |
+
+
+
+
+
+ <%= yield(:title) %>
+ <%= yield %>
+
+
+
+
+  %>)
|
+ <%= yield(:note) %>
+ |
+
+
+ |
+
+
+
+
+
+ <% unless @suppress_user_has_account_footer == true %>
+
+
+
+
+ |
+
+
+
+ This email was sent to you because you have an account at Jamkazam.
+ |
+
+ |
+
+ <% end %>
+
+
+
+ | Copyright © <%= Time.now.year %> JamKazam, Inc. All rights reserved.
+ |
+
+
+
+
+
diff --git a/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.text.erb b/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.text.erb
new file mode 100644
index 000000000..98039a7cb
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.text.erb
@@ -0,0 +1,8 @@
+<%= yield %>
+
+
+<% unless @suppress_user_has_account_footer == true %>
+This email was sent to you because you have an account at Jamkazam / http://www.jamkazam.com.
+<% end %>
+
+Copyright <%= Time.now.year %> JamKazam, Inc. All rights reserved.
diff --git a/ruby/lib/jam_ruby/app/views/layouts/user_mailer.html.erb b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.html.erb
new file mode 100644
index 000000000..b57710eea
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.html.erb
@@ -0,0 +1,59 @@
+
+
+
+
+ JamKazam
+
+
+
+
+
+
+
+
+  |
+
+
+
+
+
+ <%= yield(:title) %>
+ <%= yield %>
+
+ |
+
+
+
+
+ <% unless @suppress_user_has_account_footer == true %>
+
+
+
+
+ |
+
+
+
+ This email was sent to you because you have an account at Jamkazam.
+ |
+
+ |
+
+
+ <% end %>
+
+
+
+ | Copyright © <%= Time.now.year %> JamKazam, Inc. All rights reserved.
+ |
+
+
+
+
+
diff --git a/ruby/lib/jam_ruby/app/views/layouts/user_mailer.text.erb b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.text.erb
new file mode 100644
index 000000000..98039a7cb
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.text.erb
@@ -0,0 +1,8 @@
+<%= yield %>
+
+
+<% unless @suppress_user_has_account_footer == true %>
+This email was sent to you because you have an account at Jamkazam / http://www.jamkazam.com.
+<% 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
new file mode 100644
index 000000000..5da4b3b0a
--- /dev/null
+++ b/ruby/lib/jam_ruby/base_manager.rb
@@ -0,0 +1,31 @@
+module JamRuby
+ class BaseManager
+
+ attr_accessor :pg_conn
+
+ def initialize(options={})
+ @log = Logging.logger[self]
+ @pg_conn = options[:conn]
+
+ unless PG.threadsafe?
+ raise Exception, "a non-threadsafe build of libpq is present."
+ end
+ end
+
+ # Creates a connection manager, and associates the connection created by active_record with ourselves
+ def self.active_record_transaction
+
+ manager = self.new
+ ActiveRecord::Base.connection_pool.with_connection do |connection|
+ # create a transaction, and pass the current connection to ConnectionManager.
+ # this lets the entire operation work with the same transaction,
+ # across Rails ActiveRecord and the pg-gem based code in ConnectionManager.
+ manager.pg_conn = connection.instance_variable_get("@connection")
+
+ connection.transaction do
+ yield manager
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb
new file mode 100644
index 000000000..1161e14b4
--- /dev/null
+++ b/ruby/lib/jam_ruby/connection_manager.rb
@@ -0,0 +1,371 @@
+module JamRuby
+# All writes should occur through the ConnectionManager for the connection table
+# Reads can occur freely elsewhere, though
+# Because connections are tied to the websocket-connection and we bookkeep them in the database purely
+# for 'SQL convenience', this is a obvious place we can go away from a database
+# as an optimization if we find it's too much db traffic created'
+# At a minimum, though, we could make connections an UNLOGGED table because if the database crashes,
+# all clients should reconnect and restablish their connection anyway
+#
+# All methods in here could also be refactored as stored procedures, if we stick with a database.
+# This may make sense in the short term if we are still managing connections in the database, but
+# we move to the node-js in the websocket gateway (because the websocket gateway needs to call some of these methods).
+# Or of course we could just port the relevant methods to node-js
+#
+# Also we don't send notifications from ConnectionManager;
+# we just return enough data so that a caller can make the determination if it needs to
+
+ class ConnectionManager < BaseManager
+
+ def initialize(options={})
+ super(options)
+ @log = Logging.logger[self]
+ end
+
+ def update_staleness()
+ #TODO
+ end
+
+ ##### 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
+ end
+ return friend_ids
+ end
+
+ # reclaim the existing connection,
+ def reconnect(conn, reconnect_music_session_id)
+ music_session_id = nil
+ reconnected = false
+
+ # we will reconnect the same music_session that the connection was previously in,
+ # if it matches the same value currently in the database for music_session_id
+ music_session_id_expression = 'NULL'
+ unless reconnect_music_session_id.nil?
+ music_session_id_expression = "(CASE WHEN music_session_id='#{reconnect_music_session_id}' THEN music_session_id ELSE NULL END)"
+ end
+
+
+ sql =< 0)
+ # If a blk is passed in, on success, count is also passed back an the db connection, allowing for
+ # notifications to go out within the table log. music_session_id is also passed, if the music_session still exists
+ # and this connection was in a session
+ def delete_connection(client_id, &blk)
+
+ ConnectionManager.active_record_transaction do |connection_manager|
+ conn = connection_manager.pg_conn
+ count = 0
+ user_id = nil
+ music_session_id = nil
+
+ lock_connections(conn)
+
+ previous_music_session_id = check_already_session(conn, client_id)
+
+ conn.exec("DELETE FROM connections WHERE client_id = $1 RETURNING user_id, music_session_id", [client_id]) do |result|
+
+ if result.cmd_tuples == 0
+ # the client is already gone from the database... do nothing but log error
+ @log.warn("unable to delete client #{client_id}")
+ return
+ elsif result.cmd_tuples == 1
+ user_id = result[0]['user_id']
+ music_session_id = result[0]['music_session_id']
+
+ else
+ raise Exception, 'uniqueness constraint has been lost on client_id'
+ end
+ end
+
+ session_checks(conn, previous_music_session_id, user_id)
+
+ # since we did delete a row, check and see if any more connections for that user exist
+ # if we are down to zero, send out user gone message
+ conn.exec("SELECT count(user_id) FROM connections where user_id = $1", [user_id]) do |result|
+ count = result.getvalue(0, 0).to_i
+ end
+
+ # same for session-if we are down to the last participant, delete the session
+ unless music_session_id.nil?
+ result = conn.exec("DELETE FROM music_sessions WHERE id = $1 AND 0 = (select count(music_session_id) FROM connections where music_session_id = $1)", [music_session_id])
+ if result.cmd_tuples == 1
+ music_session_id = nil
+ end
+ end
+
+ blk.call(conn, count, music_session_id, user_id) unless blk.nil?
+ return count
+ end
+ end
+
+ def check_already_session(conn, client_id)
+ conn.exec("SELECT music_session_id FROM connections WHERE client_id = $1", [client_id]) do |result|
+ if result.num_tuples == 1
+ previous_music_session_id = result.getvalue(0, 0)
+ return previous_music_session_id
+ elsif result.num_tuples == 0
+ # there is no connection found matching this criteria; we are done.
+ @log.debug("when checking for existing session, no connection found with client=#{client_id}")
+ return nil
+ else
+ @log.error("connection table data integrity violation; multiple rows found. client_id=#{client_id}")
+ raise Exception, "connection table data integrity violation; multiple rows found. client_id=#{client_id}"
+ end
+ end
+ end
+
+ def session_checks(conn, previous_music_session_id, user_id)
+ unless previous_music_session_id.nil?
+ # TODO: send notification to friends that this user left this session?
+ @log.debug("user #{user_id} left music_session #{previous_music_session_id}")
+
+ # destroy the music_session if it's empty
+ num_participants = nil
+ 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
+ if num_participants == 0
+ # delete the music_session
+ conn.exec("DELETE from music_sessions WHERE id = $1",
+ [previous_music_session_id]) do |result|
+ if result.cmd_tuples == 1
+ # music session deleted!
+ @log.debug("deleted music session #{previous_music_session_id}")
+ JamRuby::MusicSessionHistory.removed_music_session(user_id,
+ previous_music_session_id)
+ elsif 1 < result.cmd_tuples
+ msg = "music_sessions table data integrity violation; multiple rows found with music_session_id=#{previous_music_session_id}"
+ @log.error(msg)
+ raise Exception, msg
+ end
+ end
+ end
+ end
+ end
+
+ # if a blk is passed in, upon success, it will be called and you can issue notifications
+ # within the connection table lock
+ def join_music_session(user, client_id, music_session, as_musician, tracks, &blk)
+ connection = nil
+ user_id = user.id
+ music_session_id = music_session.id
+
+ 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.music_session_id = music_session_id
+ connection.as_musician = as_musician
+ connection.joining_session = true
+ associate_tracks(connection, tracks)
+ connection.save
+
+ if connection.errors.any?
+ raise ActiveRecord::Rollback
+ else
+ blk.call(db_conn, connection) unless blk.nil?
+ MusicSessionUserHistory.save(music_session_id, user_id, client_id)
+ end
+ end
+
+ return 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)
+ ConnectionManager.active_record_transaction do |connection_manager|
+
+ conn = connection_manager.pg_conn
+
+ lock_connections(conn)
+
+ music_session_id = music_session.id
+ user_id = user.id
+ client_id = connection.client_id
+
+ previous_music_session_id = check_already_session(conn, client_id)
+
+ if previous_music_session_id == nil
+ @log.debug "the client is not in a session. user=#{user_id}, client=#{client_id}, music_session=#{music_session_id}"
+ raise StateError, "not in session"
+ elsif previous_music_session_id != music_session_id
+ @log.debug "the client is in a different session. user=#{user_id}, client=#{client_id}, music_session=#{music_session_id}"
+ raise StateError, "in a session different than that specified"
+ end
+
+ # can throw exception if the session is deleted just before this
+ conn.exec("UPDATE connections SET music_session_id = NULL, as_musician = NULL WHERE client_id = $1 AND user_id =$2", [client_id, user_id]) do |result|
+ if result.cmd_tuples == 1
+ @log.debug("disassociated music_session with connection for client_id=#{client_id}, user_id=#{user_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?
+
+ elsif result.cmd_tuples == 0
+ @log.debug "leave_music_session no connection found with client_id=#{client_id}"
+ raise ActiveRecord::RecordNotFound
+ else
+ @log.error("database failure or logic error; this path should be impossible if the table is locked (leave_music_session)")
+ raise Exception, "locked table changed state"
+ end
+ end
+ end
+ end
+
+ def lock_connections(conn)
+ conn.exec("LOCK connections IN EXCLUSIVE MODE").clear
+ end
+
+ def associate_tracks(connection, tracks)
+ @log.debug "Tracks:"
+ @log.debug tracks
+ connection.tracks.clear()
+
+ unless tracks.nil?
+ tracks.each do |track|
+ instrument = Instrument.find(track["instrument_id"])
+ t = Track.new
+ t.instrument = instrument
+ t.connection = connection
+ t.sound = track["sound"]
+ t.save
+ connection.tracks << t
+ end
+ end
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/constants/limits.rb b/ruby/lib/jam_ruby/constants/limits.rb
new file mode 100644
index 000000000..5d5e409d0
--- /dev/null
+++ b/ruby/lib/jam_ruby/constants/limits.rb
@@ -0,0 +1,18 @@
+module Limits
+
+ # band genres
+ MIN_GENRES_PER_BAND = 1
+ MAX_GENRES_PER_BAND = 1
+
+ # recording genres
+ MIN_GENRES_PER_RECORDING = 1
+ MAX_GENRES_PER_RECORDING = 1
+
+ # instruments
+ MIN_INSTRUMENTS_PER_MUSICIAN = 1
+ MAX_INSTRUMENTS_PER_MUSICIAN = 5
+
+ # users
+ USERS_CAN_INVITE = false # in BETA release, only first level users can invite others
+
+end
diff --git a/ruby/lib/jam_ruby/constants/notification_types.rb b/ruby/lib/jam_ruby/constants/notification_types.rb
new file mode 100644
index 000000000..f9faaef96
--- /dev/null
+++ b/ruby/lib/jam_ruby/constants/notification_types.rb
@@ -0,0 +1,19 @@
+module NotificationTypes
+
+ # friend notifications
+ FRIEND_UPDATE = "FRIEND_UPDATE"
+ FRIEND_REQUEST = "FRIEND_REQUEST"
+ FRIEND_REQUEST_ACCEPTED = "FRIEND_REQUEST_ACCEPTED"
+ FRIEND_SESSION_JOIN = "FRIEND_SESSION_JOIN"
+
+ # session notifications
+ SESSION_INVITATION = "SESSION_INVITATION"
+
+ # musician notifications
+ MUSICIAN_SESSION_JOIN = "MUSICIAN_SESSION_JOIN"
+ MUSICIAN_SESSION_DEPART = "MUSICIAN_SESSION_DEPART"
+
+ # recording notifications
+ RECORDING_CREATED = "RECORDING_CREATED"
+
+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
new file mode 100644
index 000000000..8f7e8b514
--- /dev/null
+++ b/ruby/lib/jam_ruby/constants/validation_messages.rb
@@ -0,0 +1,37 @@
+module ValidationMessages
+
+ # Note that these are not set up to be internationalizable
+
+ # general messages
+ PERMISSION_VALIDATION_ERROR = "You do not have permissions to perform this action."
+ USER_NOT_MUSICIAN_VALIDATION_ERROR = "You must be a Musician to perform this action."
+ USER_NOT_BAND_MEMBER_VALIDATION_ERROR = "You must be a band member to perform this action."
+
+ # band invitations
+ BAND_INVITATION_NOT_FOUND = "Band invitation not found."
+
+ # recordings
+ RECORDING_NOT_FOUND = "Recording not found."
+
+ # tracks
+ TRACK_NOT_FOUND = "Track not found."
+
+ # sessions
+ SESSION_NOT_FOUND = "Session not found."
+
+ # genres
+ GENRE_LIMIT_EXCEEDED = "No more than 1 genre is allowed."
+ GENRE_MINIMUM_NOT_MET = "At least 1 genre is required."
+
+ # instruments
+ INSTRUMENT_LIMIT_EXCEEDED = "No more than 5 instruments are allowed."
+ INSTRUMENT_MINIMUM_NOT_MET = "At least 1 instrument is required."
+
+ # user
+ OLD_PASSWORD_DOESNT_MATCH = "Your old password is incorrect."
+ EMAIL_NOT_FOUND = "Email address not found."
+ NOT_YOUR_PASSWORD = "is not your current password"
+ EMAIL_ALREADY_TAKEN = "has already been taken"
+ EMAIL_MATCHES_CURRENT = "is same as your current email"
+ INVALID_FPFILE = "is not valid"
+end
diff --git a/ruby/lib/jam_ruby/dbutil.rb b/ruby/lib/jam_ruby/dbutil.rb
new file mode 100644
index 000000000..b59c7e2bc
--- /dev/null
+++ b/ruby/lib/jam_ruby/dbutil.rb
@@ -0,0 +1,7 @@
+module JamRuby
+ class DbUtil
+ def self.create(connection_hash)
+
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/environment.rb b/ruby/lib/jam_ruby/environment.rb
new file mode 100644
index 000000000..972c834d7
--- /dev/null
+++ b/ruby/lib/jam_ruby/environment.rb
@@ -0,0 +1,21 @@
+module JamRuby
+ class Environment
+ def self.mode
+ if Object.const_defined?('Rails')
+ return Rails.env
+ else
+ # right now, there is no idea of a non-test jam-ruby usage, because it's solely a library
+ # this will need to change if we add executables to jam-ruby
+ return "test"
+ end
+ end
+
+ def self.application
+ if Object.const_defined?('Rails')
+ return 'jamweb'
+ else
+ return 'jamruby'
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/errors/jam_argument_error.rb b/ruby/lib/jam_ruby/errors/jam_argument_error.rb
new file mode 100644
index 000000000..8d9edb7df
--- /dev/null
+++ b/ruby/lib/jam_ruby/errors/jam_argument_error.rb
@@ -0,0 +1,7 @@
+module JamRuby
+ # if a bad argument is supplied.
+ # Why not use the default ruby argument error? Using this one allows us to know our API layer threw this, versus us using some core library incorrectly
+ class JamArgumentError < ArgumentError
+
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/errors/permission_error.rb b/ruby/lib/jam_ruby/errors/permission_error.rb
new file mode 100644
index 000000000..f0b4e3a2f
--- /dev/null
+++ b/ruby/lib/jam_ruby/errors/permission_error.rb
@@ -0,0 +1,5 @@
+module JamRuby
+ class PermissionError < Exception
+
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/errors/state_error.rb b/ruby/lib/jam_ruby/errors/state_error.rb
new file mode 100644
index 000000000..3b4a34a92
--- /dev/null
+++ b/ruby/lib/jam_ruby/errors/state_error.rb
@@ -0,0 +1,7 @@
+module JamRuby
+# this exception can be thrown if the server is not in a state that allows the operation to succeed
+# however, it's not necessarily a bad thing; it just means
+ class StateError < Exception
+
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/init.rb b/ruby/lib/jam_ruby/init.rb
new file mode 100644
index 000000000..adf32cf04
--- /dev/null
+++ b/ruby/lib/jam_ruby/init.rb
@@ -0,0 +1,3 @@
+# initialize actionmailer
+ActionMailer::Base.raise_delivery_errors = true
+ActionMailer::Base.view_paths = File.expand_path('../../jam_ruby/app/views/', __FILE__)
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/lib/desk_multipass.rb b/ruby/lib/jam_ruby/lib/desk_multipass.rb
new file mode 100644
index 000000000..560551473
--- /dev/null
+++ b/ruby/lib/jam_ruby/lib/desk_multipass.rb
@@ -0,0 +1,57 @@
+require 'openssl'
+require 'digest/sha1'
+require 'base64'
+require 'cgi'
+require 'time'
+require 'json'
+
+module JamRuby
+
+ # Most of the code below was taken from the example located here:
+ # https://github.com/assistly/multipass-examples/blob/master/ruby.rb
+ class DeskMultipass
+
+ API_KEY = "453ddfc0bab00130a9c13bc9a68cf24c"
+ SITE_KEY = "jamkazam"
+
+ def initialize(user)
+ @user = user
+ generate_token_and_signature
+ end
+
+ def token
+ @token
+ end
+
+ def signature
+ @signature
+ end
+
+ private
+ def generate_token_and_signature
+ key = Digest::SHA1.digest(API_KEY + SITE_KEY)[0...16]
+
+ # Generate a random 16 byte IV
+ iv = OpenSSL::Random.random_bytes(16)
+
+ json = JSON.generate(
+ :uid => @user.id,
+ :expires => (Time.now + 300).iso8601,
+ :customer_name => @user.name,
+ :customer_email => @user.email)
+
+ cipher = OpenSSL::Cipher::Cipher.new("aes-128-cbc")
+ cipher.encrypt
+ cipher.key = key
+ cipher.iv = iv
+ encrypted = cipher.update(json) + cipher.final
+
+ prepended = iv + encrypted
+ token = Base64.encode64(prepended)
+ signature = Base64.encode64(OpenSSL::HMAC.digest('sha1', API_KEY, token))
+
+ @token = CGI.escape(token)
+ @signature = CGI.escape(signature)
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/lib/profanity.rb b/ruby/lib/jam_ruby/lib/profanity.rb
new file mode 100644
index 000000000..6caaead09
--- /dev/null
+++ b/ruby/lib/jam_ruby/lib/profanity.rb
@@ -0,0 +1,40 @@
+module JamRuby
+
+ class Profanity
+ @@dictionary_file = File.join('config/profanity.yml')
+ @@dictionary = nil
+
+ def self.dictionary
+ if File.exists? @@dictionary_file
+ @@dictionary ||= YAML.load_file(@@dictionary_file)
+ else
+ @@dictionary = []
+ end
+ @@dictionary
+ end
+
+ def self.check_word(word)
+ dictionary.include?(word.downcase)
+ end
+
+ def self.is_profane?(text)
+ return false if text.nil?
+
+ text.split(/\W+/).each do |word|
+ return true if check_word(word)
+ end
+ return false
+ end
+ end
+
+
+end
+
+# This needs to be outside the module to work.
+class NoProfanityValidator < ActiveModel::EachValidator
+ # implement the method called during validation
+ def validate_each(record, attribute, value)
+ record.errors[attribute] << 'Cannot contain profanity' if Profanity.is_profane?(value)
+ end
+end
+
diff --git a/ruby/lib/jam_ruby/lib/s3_manager.rb b/ruby/lib/jam_ruby/lib/s3_manager.rb
new file mode 100644
index 000000000..32bf04d03
--- /dev/null
+++ b/ruby/lib/jam_ruby/lib/s3_manager.rb
@@ -0,0 +1,72 @@
+require 'aws-sdk'
+require 'active_support/all'
+
+module JamRuby
+ class S3Manager
+
+ SECRET = "krQP3fKpjAtWkApBEJwJJrCZ"
+
+ def self.s3_url(filename)
+ "s3://#{aws_bucket}/#{filename}"
+ end
+
+ def self.url(filename)
+ "https://s3.amazonaws.com/#{aws_bucket}/#{filename}"
+ end
+
+ def self.upload_sign(filename, content_md5, upload_id)
+ hdt = http_date_time
+ str_to_sign = "PUT\n#{content_md5}\n#{content_type}\n#{hdt}\n/#{aws_bucket}/#{filename}"
+ signature = Base64.encode64(HMAC::SHA1.digest(aws_secret_key, str_to_sign)).chomp
+ { :filename => filename, :signature => signature, :datetime => hdt, :upload_id => upload_id }
+ end
+
+ def self.hashed_filename(type, id)
+ Digest::SHA1.hexdigest "#{SECRET}_#{type}_#{id}"
+ end
+
+ def self.multipart_upload_start(upload_filename)
+ return 0 if @is_unit_test
+ s3_bucket.objects[upload_filename].multipart_upload.id
+ end
+
+ def self.multipart_upload_complete(upload_id)
+ return if @is_unit_test
+ s3_bucket.objects[upload_filename].multipart_uploads[upload_id].upload_complete(:remote_parts)
+ end
+
+ def self.delete(filename)
+ return if @is_unit_test
+ s3_bucket.objects[filename].delete
+ end
+
+ def self.set_unit_test
+ @is_unit_test = true
+ end
+
+ private
+
+ def self.s3_bucket
+ @s3 ||= AWS::S3.new
+ @s3.buckets[aws_bucket]
+ end
+
+ def self.aws_bucket
+ "jamkazam-dev"
+ end
+
+ def self.aws_secret_key
+ "XLq2mpJHNyA0bN7GBSdYyF/pWjfzGkDx92b1C+Wv"
+ end
+
+ def self.content_type
+ "application/octet-stream"
+ end
+
+ def self.http_date_time
+ Time.now.strftime("%a, %d %b %Y %H:%M:%S %z")
+ end
+
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/lib/s3_util.rb b/ruby/lib/jam_ruby/lib/s3_util.rb
new file mode 100644
index 000000000..6415ee376
--- /dev/null
+++ b/ruby/lib/jam_ruby/lib/s3_util.rb
@@ -0,0 +1,29 @@
+require 'aws-sdk'
+require 'active_support/all'
+
+module JamRuby
+ class S3Util
+ @@def_opts = { :expires => 3600 * 24, :secure => true } # 24 hours from now
+ @@s3 = AWS::S3.new(:access_key_id => ENV['AWS_KEY'], :secret_access_key => ENV['AWS_SECRET'])
+
+ def self.sign_url(bucket, path, options = @@def_opts)
+
+ bucket_gen = @@s3.buckets[bucket]
+
+ return "#{bucket_gen.objects[path].url_for(:read, :expires => options[:expires]).to_s}"
+ end
+
+ def self.url(aws_bucket, filename, options = @@def_opts)
+ "http#{options[:secure] ? "s" : ""}://s3.amazonaws.com/#{aws_bucket}/#{filename}"
+ end
+
+ def self.move(aws_bucket, source, destination)
+ @@s3.buckets[aws_bucket].objects[source].move_to[destination]
+ end
+
+ def self.delete(aws_bucket, path)
+ @@s3.buckets[aws_bucket].objects[path].delete()
+ end
+ end
+end
+
diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb
new file mode 100644
index 000000000..05c244c9b
--- /dev/null
+++ b/ruby/lib/jam_ruby/message_factory.rb
@@ -0,0 +1,231 @@
+ module JamRuby
+ # creates messages (implementation: protocol buffer) objects cleanly
+ class MessageFactory
+
+ CLIENT_TARGET = "client"
+ SERVER_TARGET = "server"
+ SESSION_TARGET_PREFIX = "session:"
+ USER_TARGET_PREFIX = "user:"
+ CLIENT_TARGET_PREFIX = "client:"
+
+ def initialize()
+ @type_values = {}
+
+ Jampb::ClientMessage::Type.constants.each do |constant|
+ @type_values[Jampb::ClientMessage::Type.const_get(constant)] = constant
+ end
+
+ end
+
+ # given a string (bytes) payload, return a client message
+ def parse_client_msg(payload)
+ return Jampb::ClientMessage.parse(payload)
+ end
+
+ # create a login message using user/pass
+ def login_with_user_pass(username, password, options = {})
+ login = Jampb::Login.new(:username => username, :password => password, :client_id => options[:client_id])
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::LOGIN, :route_to => SERVER_TARGET, :login => login)
+ end
+
+ # create a login message using token (a cookie or similar)
+ def login_with_token(token, options = {})
+ login = Jampb::Login.new(:token => token, :client_id => options[:client_id])
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::LOGIN, :route_to => SERVER_TARGET, :login => login)
+ end
+
+ # create a login ack (login was successful)
+ def login_ack(public_ip, client_id, token, heartbeat_interval, music_session_id, reconnected)
+ login_ack = Jampb::LoginAck.new(:public_ip => public_ip, :client_id => client_id, :token => token, :heartbeat_interval => heartbeat_interval, :music_session_id => music_session_id, :reconnected => reconnected)
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::LOGIN_ACK, :route_to => CLIENT_TARGET, :login_ack => login_ack)
+ end
+
+ # create a music session login message
+ def login_music_session(music_session)
+ login_music_session = Jampb::LoginMusicSession.new(:music_session => music_session)
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::LOGIN_MUSIC_SESSION, :route_to => SERVER_TARGET, :login_music_session => login_music_session)
+ end
+
+ # create a music session login message ack (success or on failure)
+ def login_music_session_ack(error, error_reason)
+ login_music_session_ack = Jampb::LoginMusicSessionAck.new(:error => error, :error_reason => error_reason)
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::LOGIN_MUSIC_SESSION_ACK, :route_to => CLIENT_TARGET, :login_music_session_ack => login_music_session_ack)
+ end
+
+ # create a music session 'leave session' message
+ def leave_music_session(music_session)
+ leave_music_session = Jampb::LeaveMusicSession.new(:music_session => music_session)
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::LEAVE_MUSIC_SESSION, :route_to => SERVER_TARGET, :leave_music_session => leave_music_session)
+ end
+
+ # create a music session leave message ack (success or on failure)
+ def leave_music_session_ack(error, error_reason)
+ leave_music_session_ack = Jampb::LeaveMusicSessionAck.new(:error => error, :error_reason => error_reason)
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::LEAVE_MUSIC_SESSION_ACK, :route_to => CLIENT_TARGET, :leave_music_session_ack => leave_music_session_ack)
+ end
+
+ # create a server bad state recovered msg
+ def server_bad_state_recovered(original_message_id)
+ recovered = Jampb::ServerBadStateRecovered.new()
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::SERVER_BAD_STATE_RECOVERED, :route_to => CLIENT_TARGET, :server_bad_state_recovered => recovered, :in_reply_to => original_message_id)
+ end
+
+ # create a server error
+ def server_generic_error(error_msg)
+ error = Jampb::ServerGenericError.new(:error_msg => error_msg)
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::SERVER_GENERIC_ERROR, :route_to => CLIENT_TARGET, :server_generic_error => error)
+ end
+
+ # create a server rejection error
+ def server_rejection_error(error_msg)
+ error = Jampb::ServerRejectionError.new(:error_msg => error_msg)
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::SERVER_REJECTION_ERROR, :route_to => CLIENT_TARGET, :server_rejection_error => error)
+ end
+
+ # create a server rejection error
+ def server_permission_error(original_message_id, error_msg)
+ error = Jampb::ServerPermissionError.new(:error_msg => error_msg)
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::SERVER_PERMISSION_ERROR, :route_to => CLIENT_TARGET, :server_permission_error => error, :in_reply_to => original_message_id)
+ end
+
+ # create a server bad state error
+ def server_bad_state_error(original_message_id, error_msg)
+ error = Jampb::ServerBadStateError.new(:error_msg => error_msg)
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::SERVER_BAD_STATE_ERROR, :route_to => CLIENT_TARGET, :server_bad_state_error => error, :in_reply_to => original_message_id)
+ end
+
+ # create a friend joined session message
+ def friend_session_join(session_id, user_id, username, photo_url)
+ join = Jampb::FriendSessionJoin.new(:session_id => session_id, :user_id => user_id, :username => username, :photo_url => photo_url)
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::FRIEND_SESSION_JOIN, :route_to => CLIENT_TARGET, :friend_session_join => join)
+ end
+
+ # create a musician joined session message
+ def musician_session_join(session_id, user_id, username, photo_url)
+ join = Jampb::MusicianSessionJoin.new(:session_id => session_id, :user_id => user_id, :username => username, :photo_url => photo_url)
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::MUSICIAN_SESSION_JOIN, :route_to => CLIENT_TARGET, :musician_session_join => join)
+ end
+
+ # create a musician left session message
+ def musician_session_depart(session_id, user_id, username, photo_url)
+ left = Jampb::MusicianSessionDepart.new(:session_id => session_id, :user_id => user_id, :username => username, :photo_url => photo_url)
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::MUSICIAN_SESSION_DEPART, :route_to => CLIENT_TARGET, :musician_session_depart => left)
+ end
+
+ # create a musician fresh session message
+ def musician_session_fresh(session_id, user_id, username, photo_url)
+ fresh = Jampb::MusicianSessionFresh.new(:session_id => session_id, :user_id => user_id, :username => username, :photo_url => photo_url)
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::MUSICIAN_SESSION_FRESH, :route_to => CLIENT_TARGET, :musician_session_fresh => fresh)
+ end
+
+ # create a musician stale session message
+ def musician_session_stale(session_id, user_id, username, photo_url)
+ stale = Jampb::MusicianSessionStale.new(:session_id => session_id, :user_id => user_id, :username => username, :photo_url => photo_url)
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::MUSICIAN_SESSION_STALE, :route_to => CLIENT_TARGET, :musician_session_stale => stale)
+ end
+
+
+ # create a user-joined session message
+ def join_request(session_id, join_request_id, username, text)
+ join_request = Jampb::JoinRequest.new(:join_request_id => join_request_id, :username => username, :text => text)
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::JOIN_REQUEST, :route_to => SESSION_TARGET_PREFIX + session_id, :join_request => join_request)
+ end
+
+ # create a test message to send in session
+ def test_session_message(session_id, msg)
+ test = Jampb::TestSessionMessage.new(:msg => msg)
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::TEST_SESSION_MESSAGE, :route_to => SESSION_TARGET_PREFIX + session_id, :test_session_message => test)
+ end
+
+ def session_invitation(receiver_id, invitation_id)
+ session_invitation = Jampb::SessionInvitation.new(:invitation => invitation_id)
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::SESSION_INVITATION, :route_to => USER_TARGET_PREFIX + receiver_id, :session_invitation => session_invitation)
+ end
+
+ # create a friend update message
+ def friend_update(user_id, name, photo_url, online, msg)
+ friend = Jampb::FriendUpdate.new(:user_id => user_id, :name => name, :photo_url => photo_url, :online => online, :msg => msg)
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::FRIEND_UPDATE, :route_to => USER_TARGET_PREFIX + user_id, :friend_update => friend)
+ end
+
+ # create a friend request message
+ def friend_request(friend_request_id, user_id, name, photo_url, friend_id, msg, notification_id, created_at)
+ friend_request = Jampb::FriendRequest.new(:friend_request_id => friend_request_id,
+ :user_id => user_id, :name => name, :photo_url => photo_url, :friend_id => friend_id, :msg => msg,
+ :notification_id => notification_id, :created_at => created_at)
+
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::FRIEND_REQUEST, :route_to => USER_TARGET_PREFIX + friend_id, :friend_request => friend_request)
+ end
+
+ # create a friend request acceptance message
+ def friend_request_accepted(friend_id, name, photo_url, user_id, msg, notification_id, created_at)
+ friend_request_accepted = Jampb::FriendRequestAccepted.new(:friend_id => friend_id,
+ :name => name, :photo_url => photo_url, :user_id => user_id, :msg => msg,
+ :notification_id => notification_id, :created_at => created_at)
+
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::FRIEND_REQUEST_ACCEPTED, :route_to => USER_TARGET_PREFIX + user_id, :friend_request_accepted => friend_request_accepted)
+ end
+
+ ############## P2P CLIENT MESSAGES #################
+
+ # send a request to do a ping
+ def ping_request(client_id, from)
+ ping_request = Jampb::PingRequest.new()
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::PING_REQUEST, :route_to => CLIENT_TARGET_PREFIX + client_id, :from => from, :ping_request => ping_request)
+ end
+
+ # respond to a ping_request with an ack
+ def ping_ack(client_id, from)
+ ping_ack = Jampb::PingAck.new()
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::PING_ACK, :route_to => CLIENT_TARGET_PREFIX + client_id, :from => from, :ping_ack => ping_ack)
+ end
+
+ # create a test message to send in session
+ def test_client_message(client_id, from, msg)
+ test = Jampb::TestClientMessage.new(:msg => msg)
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::TEST_CLIENT_MESSAGE, :route_to => CLIENT_TARGET_PREFIX + client_id, :from => from, :test_client_message => test)
+ end
+
+ ####################################################
+
+ # create a heartbeat
+ def heartbeat()
+ heartbeat = Jampb::Heartbeat.new
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::HEARTBEAT, :route_to => SERVER_TARGET, :heartbeat => heartbeat)
+ end
+
+ # create a heartbeat ack
+ def heartbeat_ack()
+ heartbeat_ack = Jampb::HeartbeatAck.new()
+ return Jampb::ClientMessage.new(:type => ClientMessage::Type::HEARTBEAT_ACK, :route_to => CLIENT_TARGET, :heartbeat_ack => heartbeat_ack)
+ end
+
+ # is this message directed to the server?
+ def server_directed? msg
+ return msg.route_to == MessageFactory::SERVER_TARGET
+ end
+
+ # is this message directed to the client?
+ def client_directed? msg
+ return msg.route_to.start_with? MessageFactory::CLIENT_TARGET_PREFIX
+ end
+
+ # is this message directed to a (music) session?
+ def session_directed? msg
+ return msg.route_to.start_with? MessageFactory::SESSION_TARGET_PREFIX
+ end
+
+ # is this message directed to a user?
+ def user_directed? msg
+ return msg.route_to.start_with? MessageFactory::USER_TARGET_PREFIX
+ end
+
+ def extract_session(msg)
+ return msg.route_to[MessageFactory::SESSION_TARGET_PREFIX..-1]
+ end
+
+ def get_message_type msg
+ return @type_values[msg.type]
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/artifact_update.rb b/ruby/lib/jam_ruby/models/artifact_update.rb
new file mode 100644
index 000000000..d60d0be41
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/artifact_update.rb
@@ -0,0 +1,29 @@
+module JamRuby
+ class ArtifactUpdate < ActiveRecord::Base
+
+ DEFAULT_ENVIRONMENT = 'public'
+
+ PRODUCTS = ['JamClient/Win32', 'JamClient/MacOSX']
+
+ self.primary_key = 'id'
+ attr_accessible :version, :uri, :sha1, :environment, :product
+
+
+ mount_uploader :uri, ArtifactUploader
+
+ validates :version, :presence => true
+ validates :uri, :presence => true
+ validates :sha1, :presence => true
+ validates :size, :presence => true
+ validates :environment, :presence => true
+ validates :product, :inclusion => {:in => PRODUCTS}
+
+ before_validation do
+ if uri.present? && uri_changed?
+ self.size = uri.file.size
+ self.sha1 = Digest::MD5.hexdigest(File.read(uri.current_path))
+ end
+ end
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/band.rb b/ruby/lib/jam_ruby/models/band.rb
new file mode 100644
index 000000000..05f0d4645
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/band.rb
@@ -0,0 +1,215 @@
+module JamRuby
+ class Band < ActiveRecord::Base
+
+ attr_accessible :name, :website, :biography, :city, :state, :country
+
+ self.primary_key = 'id'
+
+ validates :biography, no_profanity: true
+
+ # musicians
+ has_many :band_musicians, :class_name => "JamRuby::BandMusician"
+ has_many :users, :through => :band_musicians, :class_name => "JamRuby::User"
+
+ # genres
+ has_and_belongs_to_many :genres, :class_name => "JamRuby::Genre", :join_table => "bands_genres"
+
+ # recordings
+ has_many :recordings, :class_name => "JamRuby::Recording", :foreign_key => "band_id"
+
+ # likers
+ has_many :likers, :class_name => "JamRuby::BandLiker", :foreign_key => "band_id", :inverse_of => :band
+ has_many :inverse_likers, :through => :likers, :class_name => "JamRuby::User", :foreign_key => "liker_id"
+
+ # followers
+ has_many :band_followers, :class_name => "JamRuby::BandFollower", :foreign_key => "band_id"
+ has_many :followers, :through => :band_followers, :class_name => "JamRuby::User"
+ has_many :inverse_band_followers, :through => :followers, :class_name => "JamRuby::BandFollower", :foreign_key => "follower_id"
+ has_many :inverse_followers, :through => :inverse_band_followers, :source => :band, :class_name => "JamRuby::Band"
+
+ # invitations
+ has_many :invitations, :inverse_of => :band, :class_name => "JamRuby::BandInvitation", :foreign_key => "band_id"
+
+ # music_sessions
+ has_many :music_sessions, :class_name => "JamRuby::MusicSession", :foreign_key => "band_id"
+ has_many :music_session_history, :class_name => "JamRuby::MusicSessionHistory", :foreign_key => "band_id", :inverse_of => :band
+
+ def liker_count
+ return self.likers.size
+ end
+
+ def follower_count
+ return self.followers.size
+ end
+
+ def recording_count
+ return self.recordings.size
+ end
+
+ def session_count
+ return self.music_sessions.size
+ end
+
+ def location
+ 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?
+ loc
+ end
+
+ def add_member(user_id, admin)
+ BandMusician.create(:band_id => self.id, :user_id => user_id, :admin => admin)
+ end
+
+ def self.recording_index(current_user, band_id)
+ hide_private = false
+ band = Band.find(band_id)
+
+ # hide private Recordings from anyone who's not in the Band
+ unless band.users.exists? current_user
+ hide_private = true
+ end
+
+ if hide_private
+ recordings = Recording.joins(:band_recordings)
+ .where(:bands_recordings => {:band_id => "#{band_id}"}, :public => true)
+
+ else
+ recordings = Recording.joins(:band_recordings)
+ .where(:bands_recordings => {:band_id => "#{band_id}"})
+ end
+
+ return recordings
+ end
+
+ def self.search(query, options = { :limit => 10 })
+
+ # only issue search if at least 2 characters are specified
+ if query.nil? || query.length < 2
+ return []
+ end
+
+ # create 'anded' statement
+ query = Search.create_tsquery(query)
+
+ if query.nil? || query.length == 0
+ return []
+ end
+
+ return Band.where("name_tsv @@ to_tsquery('jamenglish', ?)", query).limit(options[:limit])
+ end
+
+ # helper method for creating / updating a Band
+ def self.save(id, name, website, biography, city, state, country, genres, user_id, photo_url, logo_url)
+
+ user = User.find(user_id)
+
+ # new band
+ if id.nil?
+
+ # ensure person creating this Band is a Musician
+ unless user.musician?
+ raise JamRuby::PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
+ end
+
+ validate_genres(genres, false)
+ band = Band.new()
+
+ # band update
+ else
+ validate_genres(genres, true)
+ band = Band.find(id)
+
+ # ensure user updating Band details is a Band member
+ unless band.users.exists? user
+ raise PermissionError, ValidationMessages::USER_NOT_BAND_MEMBER_VALIDATION_ERROR
+ end
+ end
+
+ # name
+ unless name.nil?
+ band.name = name
+ end
+
+ # website
+ unless website.nil?
+ band.website = website
+ end
+
+ # biography
+ unless biography.nil?
+ band.biography = biography
+ end
+
+ # city
+ unless city.nil?
+ band.city = city
+ end
+
+ # state
+ unless state.nil?
+ band.state = state
+ end
+
+ # country
+ unless country.nil?
+ band.country = country
+ end
+
+ # genres
+ unless genres.nil?
+ ActiveRecord::Base.transaction do
+ # delete all genres for this band first
+ unless band.id.nil? || band.id.length == 0
+ band.genres.delete_all
+ end
+
+ # loop through each genre in the array and save to the db
+ genres.each do |genre_id|
+ g = Genre.find(genre_id)
+ band.genres << g
+ end
+ end
+ end
+
+ # photo url
+ unless photo_url.nil?
+ band.photo_url = photo_url
+ end
+
+ # logo url
+ unless logo_url.nil?
+ band.logo_url = logo_url
+ end
+
+ band.updated_at = Time.now.getutc
+ band.save
+
+ # add the creator as the admin
+ if id.nil?
+ BandMusician.create(:band_id => band.id, :user_id => user_id, :admin => true)
+ end
+
+ return band
+ end
+
+ private
+ def self.validate_genres(genres, is_nil_ok)
+ if is_nil_ok && genres.nil?
+ return
+ end
+
+ if genres.nil?
+ raise JamRuby::JamArgumentError, ValidationMessages::GENRE_MINIMUM_NOT_MET
+ else
+ if genres.size < Limits::MIN_GENRES_PER_BAND
+ raise JamRuby::JamArgumentError, ValidationMessages::GENRE_MINIMUM_NOT_MET
+ end
+
+ if genres.size > Limits::MAX_GENRES_PER_BAND
+ raise JamRuby::JamArgumentError, ValidationMessages::GENRE_LIMIT_EXCEEDED
+ end
+ end
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/band_follower.rb b/ruby/lib/jam_ruby/models/band_follower.rb
new file mode 100644
index 000000000..adac52892
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/band_follower.rb
@@ -0,0 +1,11 @@
+module JamRuby
+ class BandFollower < ActiveRecord::Base
+
+ self.table_name = "bands_followers"
+
+ self.primary_key = 'id'
+
+ belongs_to :band, :class_name => "JamRuby::Band", :foreign_key => "band_id"
+ belongs_to :follower, :class_name => "JamRuby::User", :foreign_key => "follower_id"
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/band_following.rb b/ruby/lib/jam_ruby/models/band_following.rb
new file mode 100644
index 000000000..2f2a9cc02
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/band_following.rb
@@ -0,0 +1,11 @@
+module JamRuby
+ class BandFollowing < ActiveRecord::Base
+
+ self.table_name = "bands_followers"
+
+ self.primary_key = 'id'
+
+ belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "follower_id", :inverse_of => :inverse_band_followings
+ belongs_to :band_following, :class_name => "JamRuby::Band", :foreign_key => "band_id", :inverse_of => :band_followings
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/band_invitation.rb b/ruby/lib/jam_ruby/models/band_invitation.rb
new file mode 100644
index 000000000..4459701eb
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/band_invitation.rb
@@ -0,0 +1,56 @@
+module JamRuby
+ class BandInvitation < ActiveRecord::Base
+
+ self.table_name = "band_invitations"
+
+ self.primary_key = 'id'
+
+ BAND_INVITATION_FAN_RECIPIENT_ERROR = "A Band invitation can only be sent to a Musician."
+
+ belongs_to :receiver, :inverse_of => :received_band_invitations, :foreign_key => "user_id", :class_name => "JamRuby::User"
+ belongs_to :sender, :inverse_of => :sent_band_invitations, :foreign_key => "creator_id", :class_name => "JamRuby::User"
+ belongs_to :band, :inverse_of => :invitations, :foreign_key => "band_id", :class_name => "JamRuby::Band"
+
+ def self.save(id, band_id, user_id, creator_id, accepted)
+
+ band_invitation = BandInvitation.new()
+
+ ActiveRecord::Base.transaction do
+ # ensure certain fields are only updated on creation
+ if id.nil?
+ # ensure recipient is a Musician
+ user = User.find(user_id)
+ unless user.musician?
+ raise JamRuby::JamArgumentError, BAND_INVITATION_FAN_RECIPIENT_ERROR
+ end
+
+ band_invitation.band_id = band_id
+ band_invitation.user_id = user_id
+ band_invitation.creator_id = creator_id
+
+ # only the accepted flag can be updated after initial creation
+ else
+ band_invitation = BandInvitation.find(id)
+ band_invitation.accepted = accepted
+ end
+
+ band_invitation.updated_at = Time.now.getutc
+ band_invitation.save
+
+ # accept logic => (1) auto-friend each band member and (2) add the musician to the band
+ if accepted
+ band_musicians = BandMusician.where(:band_id => band_invitation.band.id)
+ unless band_musicians.nil?
+ band_musicians.each do |bm|
+ Friendship.save(band_invitation.receiver.id, bm.user_id)
+ end
+ end
+
+ # accepting an invitation adds the musician to the band
+ BandMusician.create(:band_id => band_invitation.band.id, :user_id => band_invitation.receiver.id, :admin => false)
+ end
+ end
+ return band_invitation
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/band_liker.rb b/ruby/lib/jam_ruby/models/band_liker.rb
new file mode 100644
index 000000000..5926d8b6c
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/band_liker.rb
@@ -0,0 +1,11 @@
+module JamRuby
+ class BandLiker < ActiveRecord::Base
+
+ self.table_name = "bands_likers"
+
+ self.primary_key = 'id'
+
+ belongs_to :band, :class_name => "JamRuby::Band", :foreign_key => "band_id", :inverse_of => :likers
+ belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "liker_id", :inverse_of => :band_likes
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/band_musician.rb b/ruby/lib/jam_ruby/models/band_musician.rb
new file mode 100644
index 000000000..91cc54c11
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/band_musician.rb
@@ -0,0 +1,31 @@
+module JamRuby
+ class BandMusician < ActiveRecord::Base
+
+ self.table_name = "bands_musicians"
+
+ attr_accessible :band_id, :user_id, :admin
+
+ self.primary_key = 'id'
+
+ belongs_to :user
+ belongs_to :band
+
+ # name, genres, photo_url, and logo_url are needed here for the RABL file
+ def name
+ @name = self.band.name
+ end
+
+ def genres
+ @genres = self.band.genres
+ end
+
+ def photo_url
+ @photo_url = self.band.photo_url
+ end
+
+ def logo_url
+ @logo_url = self.band.logo_url
+ end
+
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/claimed_recording.rb b/ruby/lib/jam_ruby/models/claimed_recording.rb
new file mode 100644
index 000000000..5e8bb963d
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/claimed_recording.rb
@@ -0,0 +1,38 @@
+module JamRuby
+ class ClaimedRecording < ActiveRecord::Base
+
+ validates :name, no_profanity: true
+
+ belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :claimed_recordings
+ belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :claimed_recordings
+ belongs_to :genre, :class_name => "JamRuby::Genre"
+ has_many :recorded_tracks, :through => :recording, :class_name => "JamRuby::RecordedTrack"
+
+ # user must own this object
+ # params is a hash, and everything is optional
+ def update_fields(user, params)
+ if user != self.user
+ raise PermissionError, "user doesn't own claimed_recording"
+ end
+
+ self.name = params[:name] unless params[:name].nil?
+ self.genre = Genre.find(params[:genre]) unless params[:genre].nil?
+ self.is_public = params[:is_public] unless params[:is_public].nil?
+ self.is_downloadable = params[:is_downloadable] unless params[:is_downloadable].nil?
+ save
+ end
+
+ def discard(user)
+ if user != self.user
+ raise PermissionError, "user doesn't own claimed_recording"
+ end
+
+ # If this is the only copy, destroy the entire recording. Otherwise, just destroy this claimed_recording
+ if recording.claimed_recordings.count == 1
+ recording.discard
+ else
+ self.destroy
+ end
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/connection.rb b/ruby/lib/jam_ruby/models/connection.rb
new file mode 100644
index 000000000..d66cfbd5a
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/connection.rb
@@ -0,0 +1,123 @@
+require 'aasm'
+
+module JamRuby
+ class Connection < ActiveRecord::Base
+
+ SELECT_AT_LEAST_ONE = "Please select at least one track"
+ FAN_CAN_NOT_JOIN_AS_MUSICIAN = "A fan can not join a music session as a musician"
+ MUSIC_SESSION_MUST_BE_SPECIFIED = "A music session must be specified"
+ INVITE_REQUIRED = "You must be invited to join this session"
+ FANS_CAN_NOT_JOIN = "Fans can not join this session"
+
+ attr_accessor :joining_session
+
+ self.primary_key = 'id'
+
+ belongs_to :user, :class_name => "JamRuby::User"
+ belongs_to :music_session, :class_name => "JamRuby::MusicSession"
+ has_many :tracks, :class_name => "JamRuby::Track", :inverse_of => :connection
+
+
+ validates :as_musician, :inclusion => {:in => [true, false]}
+ validate :can_join_music_session, :if => :joining_session?
+ after_save :require_at_least_one_track_when_in_session, :if => :joining_session?
+
+ include AASM
+ IDLE_STATE = :idle
+ CONNECT_STATE = :connected
+ STALE_STATE = :stale
+ EXPIRED_STATE = :expired
+
+ aasm do
+ state IDLE_STATE, :initial => true
+ state CONNECT_STATE
+ state STALE_STATE
+ state EXPIRED_STATE
+
+ event :connect do
+ transitions :from => IDLE_STATE, :to => CONNECT_STATE
+ transitions :from => STALE_STATE, :to => CONNECT_STATE
+ end
+ event :stale do
+ transitions :from => CONNECT_STATE, :to => STALE_STATE
+ transitions :from => IDLE_STATE, :to => STALE_STATE
+ end
+ event :expire, :after => :did_expire do
+ transitions :from => CONNECT_STATE, :to => EXPIRED_STATE
+ transitions :from => STALE_STATE, :to => EXPIRED_STATE
+ transitions :from => IDLE_STATE, :to => EXPIRED_STATE
+ end
+ end
+
+ def state_message
+ case self.aasm_state.to_sym
+ when CONNECT_STATE
+ 'Connected'
+ when STALE_STATE
+ 'Stale'
+ else
+ 'Idle'
+ end
+ end
+
+ def did_expire
+ self.destroy
+ end
+
+ def joining_session?
+ return joining_session
+ end
+
+ def can_join_music_session
+
+ if music_session.nil?
+ errors.add(:music_session, MUSIC_SESSION_MUST_BE_SPECIFIED)
+ return false
+ end
+
+ if as_musician
+ unless self.user.musician
+ errors.add(:as_musician, FAN_CAN_NOT_JOIN_AS_MUSICIAN)
+ return false
+ end
+
+ if music_session.musician_access
+ if music_session.approval_required
+ unless music_session.creator == user || music_session.invited_musicians.exists?(user)
+ errors.add(:approval_required, INVITE_REQUIRED)
+ return false
+ end
+ end
+ else
+ unless music_session.creator == user || music_session.invited_musicians.exists?(user)
+ errors.add(:musician_access, INVITE_REQUIRED)
+ return false
+ end
+ end
+ else
+ unless self.music_session.fan_access
+ # it's someone joining as a fan, and the only way a fan can join is if fan_access is true
+ errors.add(:fan_access, FANS_CAN_NOT_JOIN)
+ return false
+ end
+ end
+
+ return true
+ end
+
+
+ # decides if a given user can access this client with p2p messaging
+ # the answer is yes if the user is in the same music session
+ def access_p2p?(user)
+ return self.music_session.users.exists?(user)
+ end
+
+ private
+ def require_at_least_one_track_when_in_session
+ if tracks.count == 0
+ errors.add(:genres, SELECT_AT_LEAST_ONE)
+ end
+ end
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/crash_dump.rb b/ruby/lib/jam_ruby/models/crash_dump.rb
new file mode 100644
index 000000000..6c4e54d84
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/crash_dump.rb
@@ -0,0 +1,27 @@
+module JamRuby
+ class CrashDump < ActiveRecord::Base
+
+ self.table_name = "crash_dumps"
+
+ self.primary_key = 'id'
+
+ belongs_to :user, :inverse_of => :crash_dumps, :class_name => "JamRuby::User"
+
+ validates :client_type, presence: true
+ validates :client_version, presence: true
+
+ attr_accessor :user_email
+
+ before_validation(:on => :create) do
+ self.created_at ||= Time.now
+ self.id = SecureRandom.uuid
+ self.uri = "dump/#{self.id}-#{self.created_at.to_i}"
+ end
+
+ def user_email
+ nil if user_id.nil?
+ self.user.email
+ end
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/fan_invitation.rb b/ruby/lib/jam_ruby/models/fan_invitation.rb
new file mode 100644
index 000000000..026b16599
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/fan_invitation.rb
@@ -0,0 +1,32 @@
+module JamRuby
+ class FanInvitation < ActiveRecord::Base
+
+ 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"
+
+ self.primary_key = 'id'
+ belongs_to :sender, :inverse_of => :sent_fan_invitations, :class_name => "JamRuby::User", :foreign_key => "sender_id"
+ belongs_to :receiver, :inverse_of => :received_fan_invitations, :class_name => "JamRuby::User", :foreign_key => "receiver_id"
+ belongs_to :music_session, :inverse_of => :fan_invitations, :class_name => "JamRuby::MusicSession"
+
+ validates :sender, :presence => true
+ validates :receiver, :presence => true
+ validates :music_session, :presence => true
+
+ validate :require_sender_in_music_session, :require_are_friends
+
+ private
+
+ def require_sender_in_music_session
+ unless music_session.users.exists? sender
+ errors.add(:music_session, MEMBERSHIP_REQUIRED_OF_MUSIC_SESSION)
+ end
+ end
+
+ def require_are_friends
+ unless receiver.friends.exists? sender
+ errors.add(:receiver, FRIENDSHIP_REQUIRED_VALIDATION_ERROR)
+ end
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/feedback.rb b/ruby/lib/jam_ruby/models/feedback.rb
new file mode 100644
index 000000000..791ef7833
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/feedback.rb
@@ -0,0 +1,18 @@
+module JamRuby
+ class Feedback
+ include ActiveModel::Validations
+ include ActiveModel::Validations::Callbacks
+ include ActiveModel::Observing
+ extend ActiveModel::Callbacks
+
+ VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
+ validates :email, presence: true, format: {with: VALID_EMAIL_REGEX}
+ validates :body, :presence => true
+
+ attr_accessor :email, :body
+
+ def save
+ return valid?
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/feedback_observer.rb b/ruby/lib/jam_ruby/models/feedback_observer.rb
new file mode 100644
index 000000000..f409c753f
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/feedback_observer.rb
@@ -0,0 +1,10 @@
+module JamRuby
+ class FeedbackObserver < ActiveRecord::Observer
+
+ observe JamRuby::Feedback
+
+ def after_validation(feedback)
+ CorpMailer.feedback(feedback).deliver unless feedback.errors.any?
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/friend_request.rb b/ruby/lib/jam_ruby/models/friend_request.rb
new file mode 100644
index 000000000..fa15e36e8
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/friend_request.rb
@@ -0,0 +1,71 @@
+module JamRuby
+ class FriendRequest < ActiveRecord::Base
+
+ self.primary_key = 'id'
+
+ STATUS = %w(accept block spam ignore)
+
+ belongs_to :user, :class_name => "JamRuby::User"
+ belongs_to :friend, :class_name => "JamRuby::User"
+
+ validates :user_id, :presence => true
+ validates :friend_id, :presence => true
+ #validates :status, :inclusion => {:in => STATUS}
+ validates :message, no_profanity: true
+
+ def to_s
+ return "#{self.id} => #{self.user.to_s}:#{self.friend.to_s}"
+ end
+
+ def self.save(id, user_id, friend_id, status, message)
+ # new friend request
+ if id.nil?
+ friend_request = FriendRequest.new()
+ friend_request = validate_friend_request(friend_request, user_id, friend_id)
+ friend_request.user_id = user_id
+ friend_request.friend_id = friend_id
+ friend_request.message = message
+ friend_request.save
+
+ # send notification
+ Notification.send_friend_request(friend_request.id, user_id, friend_id)
+
+ else
+ ActiveRecord::Base.transaction do
+ friend_request = FriendRequest.find(id)
+ friend_request.status = status
+ friend_request.updated_at = Time.now.getutc
+ friend_request.save
+
+ # create both records for this friendship
+ if friend_request.status == "accept"
+ Friendship.save(friend_request.user_id, friend_request.friend_id)
+
+ # send notification
+ Notification.send_friend_request_accepted(friend_request.user_id, friend_request.friend_id)
+ end
+ end
+ end
+
+ return friend_request
+ end
+
+ private
+ def self.validate_friend_request(friend_request, user_id, friend_id)
+ friend_requests = FriendRequest.where("user_id='#{user_id}' AND friend_id='#{friend_id}'")
+
+ # check if there are any friend requests for this source/target user combo, and if
+ # any have been marked as spam or blocked, set the status of this friend request
+ # to match so it doesn't show up in the queue
+ unless friend_requests.nil? || friend_requests.size == 0
+ if friend_requests.exists?(:status => "spam")
+ friend_request.status = "spam"
+
+ elsif friend_requests.exists?(:status => "block")
+ friend_request.status = "block"
+ end
+ end
+ return friend_request
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/friendship.rb b/ruby/lib/jam_ruby/models/friendship.rb
new file mode 100644
index 000000000..52132d889
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/friendship.rb
@@ -0,0 +1,64 @@
+module JamRuby
+ class Friendship < ActiveRecord::Base
+
+ attr_accessible :user_id, :friend_id
+
+ self.primary_key = 'id'
+
+ belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id", :inverse_of => :inverse_friendships
+ belongs_to :friend, :class_name => "JamRuby::User", :foreign_key => "friend_id", :inverse_of => :friendships
+
+ def self.save(user_id, friend_id)
+ friendship = Friendship.where("user_id='#{user_id}' AND friend_id='#{friend_id}'")
+
+ if friendship.nil? || friendship.size == 0
+ Friendship.create(:user_id => user_id, :friend_id => friend_id)
+ Friendship.create(:user_id => friend_id, :friend_id => user_id)
+ end
+ end
+
+
+ # not like .save() in that it does not check for an existing friendship. The caller is responsible
+ # for checking for errors on the models
+ def self.save_using_models(user, friend)
+ this = Friendship.new
+ this.user = user
+ this.friend = friend
+
+ that = Friendship.new
+ that.user = friend
+ that.friend = user
+
+ this.save
+ that.save
+ return [this, that]
+ end
+
+ def self.search(query, user_id, options = { :limit => 10 })
+ # only issue search if at least 2 characters are specified
+ if query.nil? || query.length < 2 || user_id.nil?
+ return []
+ end
+
+ # create 'anded' statement
+ query = Search.create_tsquery(query)
+
+ if query.nil? || query.length == 0
+ return []
+ end
+
+ friends = Friendship.joins(
+ %Q{
+ INNER JOIN
+ users
+ ON friendships.friend_id = users.id
+ WHERE friendships.user_id = '#{user_id}'
+ AND users.name_tsv @@ to_tsquery('jamenglish', '#{query}')
+ }
+ )
+
+ friends = friends.limit(options[:limit])
+ return friends
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/genre.rb b/ruby/lib/jam_ruby/models/genre.rb
new file mode 100644
index 000000000..80fb3c1f8
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/genre.rb
@@ -0,0 +1,16 @@
+module JamRuby
+ class Genre < ActiveRecord::Base
+
+ self.primary_key = 'id'
+
+ # bands
+ has_and_belongs_to_many :bands, :class_name => "JamRuby::Band", :join_table => "bands_genres"
+
+ # genres
+ has_and_belongs_to_many :recordings, :class_name => "JamRuby::Recording", :join_table => "recordings_genres"
+
+ # music sessions
+ has_and_belongs_to_many :music_sessions, :class_name => "JamRuby::MusicSession", :join_table => "genres_music_sessions"
+
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/instrument.rb b/ruby/lib/jam_ruby/models/instrument.rb
new file mode 100644
index 000000000..6b5936e87
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/instrument.rb
@@ -0,0 +1,20 @@
+module JamRuby
+ class Instrument < ActiveRecord::Base
+
+ self.primary_key = 'id'
+
+ # users
+ has_many :musician_instruments, :class_name => "JamRuby::MusicianInstrument"
+ has_many :users, :through => :musician_instruments, :class_name => "JamRuby::User"
+ has_many :tracks, :class_name => "JamRuby::Track", :inverse_of => :instrument
+ has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :inverse_of => :instrument
+
+ # music sessions
+ has_and_belongs_to_many :music_sessions, :class_name => "JamRuby::MusicSession", :join_table => "genres_music_sessions"
+
+ def self.standard_list
+ return Instrument.where('instruments.popularity > 0').order('instruments.popularity DESC, instruments.description ASC')
+ end
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/invitation.rb b/ruby/lib/jam_ruby/models/invitation.rb
new file mode 100644
index 000000000..9bd998d56
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/invitation.rb
@@ -0,0 +1,39 @@
+module JamRuby
+ class Invitation < ActiveRecord::Base
+
+ 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"
+
+ self.primary_key = 'id'
+ belongs_to :sender, :inverse_of => :sent_invitations, :class_name => "JamRuby::User", :foreign_key => "sender_id"
+ belongs_to :receiver, :inverse_of => :received_invitations, :class_name => "JamRuby::User", :foreign_key => "receiver_id"
+ belongs_to :music_session, :inverse_of => :invitations, :class_name => "JamRuby::MusicSession"
+ belongs_to :join_request, :inverse_of => :invitations, :class_name => "JamRuby::JoinRequest"
+
+ validates :sender, :presence => true
+ validates :receiver, :presence => true
+ validates :music_session, :presence => true
+
+ validate :require_sender_in_music_session, :require_are_friends_or_requested_to_join
+
+ private
+
+ def require_sender_in_music_session
+ unless music_session.users.exists? sender
+ errors.add(:music_session, MEMBERSHIP_REQUIRED_OF_MUSIC_SESSION)
+ end
+ end
+
+ def require_are_friends_or_requested_to_join
+ 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
+ errors.add(:receiver, FRIENDSHIP_REQUIRED_VALIDATION_ERROR)
+ end
+ end
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/invited_user.rb b/ruby/lib/jam_ruby/models/invited_user.rb
new file mode 100644
index 000000000..47069c43e
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/invited_user.rb
@@ -0,0 +1,66 @@
+module JamRuby
+ class InvitedUser < ActiveRecord::Base
+
+ VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
+
+ attr_accessible :email, :sender_id, :autofriend, :note
+ attr_accessor :accepted_twice
+
+ self.primary_key = 'id'
+
+ ### Who sent this invitatio?
+ # either admin_sender or user_sender is not null. If an administrator sends the invitation, then
+ belongs_to :sender , :inverse_of => :invited_users, :class_name => "JamRuby::User", :foreign_key => "sender_id"
+
+ # who is the invitation sent to?
+ validates :email, :presence => true, format: {with: VALID_EMAIL_REGEX}
+ validates :autofriend, :inclusion => {:in => [nil, true, false]}
+ validates :invitation_code, :presence => true
+ validates :note, length: {maximum: 400}, no_profanity: true # 400 == arbitrary.
+
+ validate :valid_personalized_invitation
+ validate :not_accepted_twice
+ validate :can_invite?
+
+ # ensure invitation code is always created
+ before_validation(:on => :create) do
+ self.invitation_code = SecureRandom.urlsafe_base64 if self.invitation_code.nil?
+ self.sender_id = nil if self.sender_id.blank? # this coercion was done just to make activeadmin work
+ end
+
+ def self.index(user)
+ return InvitedUser.where(:sender_id => user).order(:updated_at)
+ end
+
+ def sender_display_name
+ return sender.name
+ end
+
+ def accept!
+ if self.accepted
+ accepted_twice = true
+ end
+
+ self.accepted = true
+ end
+
+ def invited_by_administrator?
+ sender.nil? || sender.admin # a nil sender can only be created by someone using jam-admin
+ end
+ private
+
+ def can_invite?
+ errors.add(:sender, "can not invite others") if !invited_by_administrator? && !sender.can_invite?
+ end
+
+ def valid_personalized_invitation
+ errors.add(:autofriend, "must be true if sender is specified") if autofriend && sender.nil?
+ end
+
+ def not_accepted_twice
+ errors.add(:accepted, "you can only accept an invitation once") if accepted_twice
+ end
+
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/invited_user_observer.rb b/ruby/lib/jam_ruby/models/invited_user_observer.rb
new file mode 100644
index 000000000..a4a007a93
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/invited_user_observer.rb
@@ -0,0 +1,14 @@
+module JamRuby
+ class InvitedUserObserver < ActiveRecord::Observer
+
+ observe JamRuby::InvitedUser
+
+ def after_create(invited_user)
+ if invited_user.sender.nil?
+ InvitedUserMailer.welcome_betauser(invited_user).deliver
+ else
+ InvitedUserMailer.friend_invitation(invited_user).deliver
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/join_request.rb b/ruby/lib/jam_ruby/models/join_request.rb
new file mode 100644
index 000000000..4561f03c7
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/join_request.rb
@@ -0,0 +1,44 @@
+module JamRuby
+ class JoinRequest < ActiveRecord::Base
+
+ REQUESTOR_MUST_BE_A_MUSICIAN = "requestor must be a musician"
+
+ self.primary_key = 'id'
+
+ belongs_to :user, :class_name => "JamRuby::User"
+ belongs_to :music_session, :class_name => "JamRuby::MusicSession"
+ has_many :invitations, :inverse_of => :join_request, :class_name => "JamRuby::Invitation"
+
+ validates :user, :presence => true
+ validates :music_session, :presence => true
+ validates :text, presence: false, no_profanity: true, length: {maximum: 140} # arbitrary decision of 140. the database is at 2000 max on this field
+
+ validates_uniqueness_of :user_id, :scope => :music_session_id
+
+ validate :requestor_is_musician
+
+ # list all paginations for the current user
+ def self.index(current_user)
+ # TODO pagination
+ return JoinRequest.where("join_requests.user_id = '#{current_user.id}'").order('join_requests.created_at DESC')
+ end
+
+ def requestor_is_musician
+ unless user.musician?
+ errors.add(:user, REQUESTOR_MUST_BE_A_MUSICIAN)
+ end
+ end
+
+ def to_s
+ return "#{self.user.to_s}:#{self.music_session.to_s}"
+ end
+
+
+ # permissions:
+ # only the creator of the join request can do a get
+ # or a member of the music_session that the join_request is designated for
+ def self.show(id, user)
+ return JoinRequest.find(id, :conditions => ["user_id = ? OR music_session_id IN (select music_session_id from connections WHERE user_id = ?)", user.id, user.id])
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/max_mind_geo.rb b/ruby/lib/jam_ruby/models/max_mind_geo.rb
new file mode 100644
index 000000000..0c8f2c082
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/max_mind_geo.rb
@@ -0,0 +1,38 @@
+module JamRuby
+ class MaxMindGeo < ActiveRecord::Base
+
+ self.table_name = 'max_mind_geo'
+
+
+ def self.import_from_max_mind(file)
+ # File Geo-124
+ # Format:
+ # startIpNum,endIpNum,country,region,city,postalCode,latitude,longitude,dmaCode,areaCode
+
+ MaxMindGeo.transaction do
+ MaxMindGeo.delete_all
+ File.open(file, 'r:ISO-8859-1') do |io|
+ MaxMindGeo.pg_copy_from io, :map => { 'startIpNum' => 'ip_bottom', 'endIpNum' => 'ip_top', 'country' => 'country', 'region' => 'region', 'city' => 'city'}, :columns => [:startIpNum, :endIpNum, :country, :region, :city] do |row|
+ row[0] = ip_address_to_int(row[0])
+ row[1] = ip_address_to_int(row[1])
+ row.delete_at(5)
+ row.delete_at(5)
+ row.delete_at(5)
+ row.delete_at(5)
+ row.delete_at(5)
+ end
+ end
+ end
+ end
+
+
+ # Make an IP address fit in a signed int. Just divide it by 2, as the least significant part
+ # just can't possibly matter. We can verify this if needed. My guess is the entire bottom octet is
+ # actually irrelevant
+ def self.ip_address_to_int(ip)
+ ip.split('.').inject(0) {|total,value| (total << 8 ) + value.to_i} / 2
+ end
+ end
+
+
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/max_mind_isp.rb b/ruby/lib/jam_ruby/models/max_mind_isp.rb
new file mode 100644
index 000000000..2c7d6ed9a
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/max_mind_isp.rb
@@ -0,0 +1,57 @@
+module JamRuby
+ class MaxMindIsp < ActiveRecord::Base
+
+ self.table_name = 'max_mind_isp'
+
+ def self.import_from_max_mind(file)
+
+ # File Geo-142
+ # Format:
+ # "beginIp","endIp","countryCode","ISP"
+
+ MaxMindIsp.transaction do
+ MaxMindIsp.delete_all
+ File.open(file, 'r:ISO-8859-1') do |io|
+ io.gets # eat the copyright line. gah, why do they have that in their file??
+ MaxMindIsp.pg_copy_from io, :map => { 'beginIp' => 'ip_bottom', 'endIp' => 'ip_top', 'countryCode' => 'country', 'ISP' => 'isp'}, :columns => [:beginIp, :endIp, :countryCode, :ISP] do |row|
+ row[0] = ip_address_to_int(strip_quotes(row[0]))
+ row[1] = ip_address_to_int(strip_quotes(row[1]))
+ row[2] = row[2]
+ row[3] = row[3..-1].join(',') # this is because the parser just cuts on any ',' and ignores double quotes. essentially postgres-copy isn't a great csv parser -- or I need to configure it better
+ while row.length > 4
+ row.delete_at(4)
+ end
+
+ end
+ end
+ end
+ end
+
+ # Make an IP address fit in a signed int. Just divide it by 2, as the least significant part
+ # just can't possibly matter. We can verify this if needed. My guess is the entire bottom octet is
+ # actually irrelevant
+ def self.ip_address_to_int(ip)
+ ip.split('.').inject(0) {|total,value| (total << 8 ) + value.to_i} / 2
+ end
+
+ private
+
+ def self.strip_quotes str
+ return nil if str.nil?
+
+ if str.chr == '"'
+ str = str[1..-1]
+ end
+
+ if str.rindex('"') == str.length - 1
+ str = str.chop
+ end
+
+ return str
+ end
+
+ def self.escape str
+ str.gsub(/\"/, '""')
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/mix.rb b/ruby/lib/jam_ruby/models/mix.rb
new file mode 100644
index 000000000..d1add4045
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/mix.rb
@@ -0,0 +1,71 @@
+module JamRuby
+ class Mix < ActiveRecord::Base
+ MAX_MIX_TIME = 7200 # 2 hours
+
+ before_destroy :delete_s3_files
+
+ self.primary_key = 'id'
+ belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :mixes
+
+ def self.schedule(recording, manifest)
+ raise if recording.nil?
+
+ mix = Mix.new
+ mix.recording = recording
+ mix.manifest = manifest
+ mix.save
+
+ mix
+ end
+
+ def self.next(mix_server)
+ # First check if there are any mixes started so long ago that we want to re-run them
+ Mix.where("completed_at IS NULL AND started_at < ?", Time.now - MAX_MIX_TIME).each do |mix|
+ # FIXME: This should probably throw some kind of log, since it means something went wrong
+ mix.started_at = nil
+ mix.mix_server = nil
+ mix.save
+ end
+
+ mix = Mix.where(:started_at => nil).limit(1).first
+ return nil if mix.nil?
+
+ mix.started_at = Time.now
+ mix.mix_server = mix_server
+ mix.save
+
+ mix
+ end
+
+ def finish(length, md5)
+ self.completed_at = Time.now
+ self.length = length
+ self.md5 = md5
+ save
+ end
+
+ def s3_url
+ S3Manager.s3_url(hashed_filename)
+ end
+
+ def url
+ S3Manager.url(hashed_filename)
+ end
+
+ def is_completed
+ !completed_at.nil?
+ end
+
+ private
+
+ def delete_s3_files
+ S3Manager.delete(hashed_filename)
+ end
+
+ def hashed_filename
+ S3Manager.hashed_filename('mix', id)
+ end
+
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb
new file mode 100644
index 000000000..ba4054dff
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/music_session.rb
@@ -0,0 +1,194 @@
+module JamRuby
+ class MusicSession < ActiveRecord::Base
+
+ self.primary_key = 'id'
+
+ attr_accessor :legal_terms, :skip_genre_validation
+ attr_accessible :creator, :description, :musician_access, :approval_required, :fan_chat, :fan_access, :genres
+
+ belongs_to :creator, :inverse_of => :music_sessions, :class_name => "JamRuby::User", :foreign_key => "user_id"
+
+ has_many :connections, :class_name => "JamRuby::Connection"
+ has_many :users, :through => :connections, :class_name => "JamRuby::User"
+ has_and_belongs_to_many :genres, :class_name => "::JamRuby::Genre", :join_table => "genres_music_sessions"
+ has_many :join_requests, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::JoinRequest"
+ has_many :invitations, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::Invitation"
+ has_many :invited_musicians, :through => :invitations, :class_name => "JamRuby::User", :foreign_key => "receiver_id", :source => :receiver
+
+ has_many :fan_invitations, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::FanInvitation"
+ has_many :invited_fans, :through => :fan_invitations, :class_name => "JamRuby::User", :foreign_key => "receiver_id", :source => :receiver
+ has_one :recording, :class_name => "JamRuby::Recording", :inverse_of => :music_session
+
+ belongs_to :band, :inverse_of => :music_sessions, :class_name => "JamRuby::Band", :foreign_key => "band_id"
+
+ after_save :require_at_least_one_genre, :limit_max_genres
+
+ after_destroy do |obj|
+ JamRuby::MusicSessionHistory.removed_music_session(obj.user_id, obj.id)
+ end
+
+ validates :description, :presence => true, :no_profanity => true
+ validates :fan_chat, :inclusion => {:in => [true, false]}
+ validates :fan_access, :inclusion => {:in => [true, false]}
+ validates :approval_required, :inclusion => {:in => [true, false]}
+ validates :musician_access, :inclusion => {:in => [true, false]}
+ validates :legal_terms, :inclusion => {:in => [true]}, :on => :create
+ validates :creator, :presence => true
+ validate :creator_is_musician
+
+ def creator_is_musician
+ unless creator.musician?
+ errors.add(:creator, "creator must be a musician")
+ end
+ end
+
+ # This is a little confusing. You can specify *BOTH* friends_only and my_bands_only to be true
+ # If so, then it's an OR condition. If both are false, you can get sessions with anyone.
+ def self.index(current_user, participants = nil, genres = nil, friends_only = false, my_bands_only = false, keyword = nil)
+
+ query = MusicSession
+ .joins(
+ %Q{
+ LEFT OUTER JOIN
+ connections
+ ON
+ music_sessions.id = connections.music_session_id
+ }
+ )
+ .joins(
+ %Q{
+ LEFT OUTER JOIN
+ friendships
+ ON
+ connections.user_id = friendships.user_id
+ AND
+ friendships.friend_id = '#{current_user.id}'
+ }
+ )
+ .joins(
+ %Q{
+ LEFT OUTER JOIN
+ invitations
+ ON
+ invitations.music_session_id = music_sessions.id
+ AND
+ invitations.receiver_id = '#{current_user.id}'
+ }
+ )
+ .group(
+ %Q{
+ music_sessions.id
+ }
+ )
+ .order(
+ %Q{
+ SUM(CASE WHEN invitations.id IS NULL THEN 0 ELSE 1 END) DESC,
+ SUM(CASE WHEN friendships.user_id IS NULL THEN 0 ELSE 1 END) DESC,
+ music_sessions.created_at DESC
+ }
+ )
+ .where(
+ %Q{
+ musician_access = true
+ OR
+ invitations.id IS NOT NULL
+ }
+ )
+
+ query = query.where("music_sessions.description like '%#{keyword}%'") unless keyword.nil?
+ query = query.where("connections.user_id" => participants.split(',')) unless participants.nil?
+ query = query.joins(:genres).where("genres.id" => genres.split(',')) unless genres.nil?
+
+ if my_bands_only
+ query = query.joins(
+ %Q{
+ LEFT OUTER JOIN
+ bands_musicians
+ ON
+ bands_musicians.user_id = '#{current_user.id}'
+ }
+ )
+ end
+
+ if my_bands_only || friends_only
+ query = query.where(
+ %Q{
+ #{friends_only ? "friendships.user_id IS NOT NULL" : "false"}
+ OR
+ #{my_bands_only ? "bands_musicians.band_id = music_sessions.band_id" : "false"}
+ }
+ )
+ end
+
+ return query
+ end
+
+ # Verifies that the specified user can join this music session
+ def can_join? user, as_musician
+ if as_musician
+ if !user.musician
+ return false # "a fan can not join a music session as a musician"
+ raise PermissionError, "a fan can not join a music session as a musician"
+ end
+
+ if self.musician_access
+ if self.approval_required
+ return self.invited_musicians.exists?(user)
+ else
+ return true
+ end
+
+ else
+ # the creator can always join, and the invited users can join
+ return self.creator == user || self.invited_musicians.exists?(user)
+ end
+ else
+ # it's a fan, and the only way a fan can join is if fan_access is true
+ return self.fan_access
+ end
+
+ end
+
+ # Verifies that the specified user can see this music session
+ def can_see? user
+ if self.musician_access
+ return true
+ else
+ # the creator can always see, and the invited users can see it too
+ return self.creator == user || self.invited_musicians.exists?(user)
+ end
+ end
+
+ # Verifies that the specified user can delete this music session
+ def can_delete? user
+ # the creator can delete
+ return self.creator == user
+ end
+
+ def access? user
+ return self.users.exists? user
+ end
+
+ def to_s
+ return description
+ end
+
+ private
+
+ def require_at_least_one_genre
+ unless skip_genre_validation
+ if self.genres.count < Limits::MIN_GENRES_PER_RECORDING
+ errors.add(:genres, ValidationMessages::GENRE_MINIMUM_NOT_MET)
+ end
+ end
+ end
+
+ def limit_max_genres
+ unless skip_genre_validation
+ if self.genres.count > Limits::MAX_GENRES_PER_RECORDING
+ errors.add(:genres, ValidationMessages::GENRE_LIMIT_EXCEEDED)
+ end
+ end
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/music_session_history.rb b/ruby/lib/jam_ruby/models/music_session_history.rb
new file mode 100644
index 000000000..fe8e7d082
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/music_session_history.rb
@@ -0,0 +1,107 @@
+module JamRuby
+ class MusicSessionHistory < ActiveRecord::Base
+
+ self.table_name = "music_sessions_history"
+
+ self.primary_key = 'id'
+
+ # for some reason the association is not working, i suspect has to do with the foreign key
+ def music_session_user_histories
+ @msuh ||= JamRuby::MusicSessionUserHistory
+ .where(:music_session_id => self.music_session_id)
+ .order('created_at DESC')
+ end
+ # has_many(:music_session_user_histories,
+ # :class_name => "JamRuby::MusicSessionUserHistory",
+ # :foreign_key => :music_session_id,
+ # :order => 'created_at DESC',
+ # :inverse_of => :music_session_history)
+
+ has_one(:perf_data,
+ :class_name => "JamRuby::MusicSessionPerfData",
+ :foreign_key => "music_session_id",
+ :inverse_of => :music_session)
+
+ belongs_to(:user,
+ :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_session_history)
+
+ GENRE_SEPARATOR = '|'
+
+ def self.index(current_user, user_id, band_id = nil, genre = nil)
+ hide_private = false
+ if current_user.id != user_id
+ hide_private = false # TODO: change to true once public flag exists
+ end
+
+ query = MusicSessionHistory
+ .joins(
+ %Q{
+ LEFT OUTER JOIN
+ music_sessions_user_history
+ ON
+ music_sessions_history.music_session_id = music_sessions_user_history.music_session_id
+ }
+ )
+ .where(
+ %Q{
+ music_sessions_history.user_id = '#{user_id}'
+ }
+ )
+
+ #query = query.where("public = false") unless !hide_private
+ query = query.where("music_sessions_history.band_id = '#{band_id}") unless band_id.nil?
+ query = query.where("music_sessions_history.genres like '%#{genre}%'") unless genre.nil?
+ return query
+ end
+
+ def unique_users
+ User
+ .joins(:music_session_user_histories)
+ .group("users.id")
+ .order("users.id")
+ .where(%Q{ music_sessions_user_history.music_session_id = '#{music_session_id}'})
+ end
+
+ def self.save(music_session)
+ session_history = MusicSessionHistory.find_by_music_session_id(music_session.id)
+
+ if session_history.nil?
+ session_history = MusicSessionHistory.new()
+ end
+
+ session_history.music_session_id = music_session.id
+ session_history.description = music_session.description unless music_session.description.nil?
+ session_history.user_id = music_session.creator.id
+ session_history.band_id = music_session.band.id unless music_session.band.nil?
+ session_history.genres = music_session.genres.map { |g| g.id }.join GENRE_SEPARATOR
+ session_history.save!
+ end
+
+ def self.removed_music_session(user_id, session_id)
+ hist = self
+ .where(:user_id => user_id)
+ .where(:music_session_id => session_id)
+ .limit(1)
+ .first
+ hist.update_attribute(:session_removed_at, Time.now) if hist
+ JamRuby::MusicSessionUserHistory.removed_music_session(user_id, session_id)
+ end
+
+ def duration_minutes
+ end_time = self.session_removed_at || Time.now
+ (end_time - self.created_at) / 60.0
+ end
+
+ def perf_uri
+ self.perf_data.try(:uri)
+ end
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/music_session_perf_data.rb b/ruby/lib/jam_ruby/models/music_session_perf_data.rb
new file mode 100644
index 000000000..1133cb28e
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/music_session_perf_data.rb
@@ -0,0 +1,28 @@
+require 'securerandom'
+
+module JamRuby
+ class MusicSessionPerfData < ActiveRecord::Base
+
+ self.primary_key = 'id'
+
+ attr_accessible :uri
+
+ belongs_to(:music_session,
+ :class_name => "JamRuby::MusicSessionHistory",
+ :foreign_key => :music_session_id,
+ :inverse_of => :perf_data)
+
+ # mount_uploader :uri, PerfDataUploader
+
+ validates :music_session, :presence => true
+ validates :client_id, :presence => true
+ validates :uri, :presence => true
+
+ before_validation(:on => :create) do
+ self.created_at ||= Time.now
+ self.id = SecureRandom.uuid
+ self.uri = "perf_data/#{self.music_session_id}/#{self.client_id}-#{self.created_at.to_i}"
+ end
+ end
+
+end
diff --git a/ruby/lib/jam_ruby/models/music_session_user_history.rb b/ruby/lib/jam_ruby/models/music_session_user_history.rb
new file mode 100644
index 000000000..f4de39dc1
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/music_session_user_history.rb
@@ -0,0 +1,52 @@
+module JamRuby
+ class MusicSessionUserHistory < ActiveRecord::Base
+
+ self.table_name = "music_sessions_user_history"
+
+ self.primary_key = 'id'
+
+ belongs_to(:user,
+ :class_name => "JamRuby::User",
+ :foreign_key => "user_id",
+ :inverse_of => :music_session_user_histories)
+
+ # for some reason the association is not working, i suspect has to do with the foreign key
+ def music_session_history
+ @msh ||= JamRuby::MusicSessionHistory
+ .where(:music_session_id => self.music_session_id)
+ .limit(1)
+ .first
+ end
+ # belongs_to(:music_session_history,
+ # :class_name => "JamRuby::MusicSessionHistory",
+ # :foreign_key => :music_session_id,
+ # :inverse_of => :music_session_user_histories)
+
+ def self.save(music_session_id, user_id, client_id)
+ session_user_history = MusicSessionUserHistory.new()
+ session_user_history.music_session_id = music_session_id
+ session_user_history.user_id = user_id
+ session_user_history.client_id = client_id
+ session_user_history.save
+ end
+
+ def user_email
+ self.user ? self.user.email : ''
+ end
+
+ def duration_minutes
+ end_time = self.session_removed_at || Time.now
+ (end_time - self.created_at) / 60.0
+ end
+
+ def self.removed_music_session(user_id, session_id)
+ hist = self
+ .where(:user_id => user_id)
+ .where(:music_session_id => session_id)
+ .limit(1)
+ .first
+ hist.update_attribute(:session_removed_at, Time.now) if hist
+ end
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/musician_instrument.rb b/ruby/lib/jam_ruby/models/musician_instrument.rb
new file mode 100644
index 000000000..503a4e6f3
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/musician_instrument.rb
@@ -0,0 +1,18 @@
+module JamRuby
+ class MusicianInstrument < ActiveRecord::Base
+
+ self.table_name = "musicians_instruments"
+
+ self.primary_key = 'id'
+
+ # ensure most proficient, highest priority
+ default_scope order('proficiency_level DESC, priority ASC')
+
+ belongs_to :user, :class_name => "JamRuby::User"
+ belongs_to :instrument, :class_name => "JamRuby::Instrument"
+
+ def description
+ @description = self.instrument.description
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb
new file mode 100644
index 000000000..0afc93ad5
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/notification.rb
@@ -0,0 +1,280 @@
+module JamRuby
+ class Notification < ActiveRecord::Base
+
+ self.primary_key = 'id'
+
+ default_scope order('created_at DESC')
+
+ belongs_to :target_user, :class_name => "JamRuby::User", :foreign_key => "target_user_id"
+ 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 :session, :class_name => "JamRuby::MusicSession", :foreign_key => "session_id"
+ belongs_to :recording, :class_name => "JamRuby::Recording", :foreign_key => "recording_id"
+
+ def index(user_id)
+ results = Notification.where(:target_user_id => user_id).limit(50)
+ return results
+ end
+
+ def photo_url
+ unless self.source_user.nil?
+ self.source_user.photo_url
+ end
+ end
+
+ # used for persisted notifications
+ def formatted_msg
+ target_user, source_user, band, session, recording, invitation, join_request = nil
+
+ unless self.target_user_id.nil?
+ target_user = User.find(self.target_user_id)
+ end
+
+ unless self.source_user_id.nil?
+ source_user = User.find(self.source_user_id)
+ end
+
+ unless self.band_id.nil?
+ band = Band.find(self.band_id)
+ end
+
+ unless self.session_id.nil?
+ session = MusicSession.find(self.session_id)
+ end
+
+ unless self.recording_id.nil?
+ recording = Recording.find(self.recording_id)
+ end
+
+ unless self.invitation_id.nil?
+ invitation = Invitation.find(self.invitation_id)
+ end
+
+ unless self.join_request_id.nil?
+ join_request = JoinRequest.find(self.join_request_id)
+ end
+
+ return self.class.format_msg(self.description, source_user)
+ end
+
+ # TODO: MAKE ALL METHODS BELOW ASYNC SO THE CLIENT DOESN'T BLOCK ON NOTIFICATION LOGIC
+ # TODO: ADD TESTS FOR THIS CLASS
+
+ class << self
+
+ @@mq_router = MQRouter.new
+ @@message_factory = MessageFactory.new
+
+ def delete_all(session_id)
+ Notification.delete_all "(session_id = '#{session_id}')"
+ end
+
+ ################### HELPERS ###################
+ def retrieve_friends(connection, user_id)
+ friend_ids = []
+ connection.exec("SELECT f.friend_id as friend_id FROM friendships f WHERE f.user_id = $1", [user_id]) do |friend_results|
+ friend_results.each do |friend_result|
+ friend_ids.push(friend_result['friend_id'])
+ end
+ end
+ return friend_ids
+ end
+
+ def retrieve_followers(connection, user_id)
+ follower_ids = []
+ connection.exec("SELECT uf.follower_id as friend_id FROM users_followers uf WHERE uf.user_id = $1", [user_id]) do |follower_results|
+ follower_results.each do |follower_result|
+ follower_ids.push(follower_result['follower_id'])
+ end
+ end
+ return follower_ids
+ end
+
+ def retrieve_friends_and_followers(connection, user_id)
+ ids = retrieve_friends(connection, user_id)
+ ids.concat(retrieve_followers(connection, user_id))
+ ids.uniq! {|id| id}
+ return ids
+ end
+
+ def retrieve_friends_and_followers_not_in_session(connection, user_id, session_id)
+ ids = retrieve_friends_and_followers(connection, user_id)
+ connection.exec("SELECT c.user_id as musician_id FROM connections c WHERE c.music_session_id = $1", [session_id]) do |musicians|
+ musicians.each do |musician_result|
+ # remove users who are in the session
+ ids.reject! {|item| item == musician_result['musician_id']}
+ end
+ end
+ return ids
+ end
+
+ def format_msg(description, user)
+ case description
+ when NotificationTypes::FRIEND_UPDATE
+ return "#{user.name} is now "
+
+ when NotificationTypes::FRIEND_REQUEST
+ return "#{user.name} has sent you a friend request."
+
+ when NotificationTypes::FRIEND_REQUEST_ACCEPTED
+ return "#{user.name} has accepted your friend request."
+
+ when NotificationTypes::FRIEND_SESSION_JOIN
+ return "#{user.name} has joined the session."
+
+ when NotificationTypes::MUSICIAN_SESSION_JOIN
+ return "#{user.name} has joined the session."
+
+ when NotificationTypes::MUSICIAN_SESSION_DEPART
+ return "#{user.name} has left the session."
+
+ # when "social_media_friend_joined"
+ # when "join_request_approved"
+ # when "join_request_rejected"
+ # when "session_invitation"
+ # when "band_invitation"
+ # when "band_invitation_accepted"
+ # when "recording_available"
+ else
+ return ""
+ end
+ end
+
+ ################### FRIEND UPDATE ###################
+ def send_friend_update(user_id, online, connection)
+
+ # (1) get all of this user's friends
+ friend_ids = retrieve_friends(connection, user_id)
+
+ if friend_ids.length > 0
+ user = User.find(user_id)
+
+ # (2) create notification
+ online_msg = online ? "online." : "offline."
+ notification_msg = format_msg(NotificationTypes::FRIEND_UPDATE, user) + online_msg
+ msg = @@message_factory.friend_update(user_id, user.name, user.photo_url, online, notification_msg)
+
+ # (3) send notification
+ @@mq_router.publish_to_friends(friend_ids, msg, user_id)
+ end
+ end
+
+ ################### FRIEND REQUEST ###################
+ def send_friend_request(friend_request_id, user_id, friend_id)
+ user = User.find(user_id)
+
+ # (1) save to database
+ notification = Notification.new
+ notification.description = NotificationTypes::FRIEND_REQUEST
+ notification.source_user_id = user_id
+ notification.target_user_id = friend_id
+ notification.friend_request_id = friend_request_id
+ notification.save
+
+ # (2) create notification
+ notification_msg = format_msg(NotificationTypes::FRIEND_REQUEST, user)
+ msg = @@message_factory.friend_request(friend_request_id, user_id, user.name, user.photo_url, friend_id, notification_msg, notification.id, notification.created_at.to_s)
+
+ # (3) send notification
+ @@mq_router.publish_to_user(friend_id, msg)
+ end
+
+ ############### FRIEND REQUEST ACCEPTED ###############
+ def send_friend_request_accepted(user_id, friend_id)
+ friend = User.find(friend_id)
+
+ # (1) save to database
+ notification = Notification.new
+ notification.description = NotificationTypes::FRIEND_REQUEST_ACCEPTED
+ notification.source_user_id = friend_id
+ notification.target_user_id = user_id
+ notification.save
+
+ # (2) create notification
+ notification_msg = format_msg(NotificationTypes::FRIEND_REQUEST_ACCEPTED, friend)
+ msg = @@message_factory.friend_request_accepted(friend_id, friend.name, friend.photo_url, user_id, notification_msg, notification.id, notification.created_at.to_s)
+
+ # (3) send notification
+ @@mq_router.publish_to_user(user_id, msg)
+ end
+
+ ################## SESSION INVITATION ##################
+ def send_session_invitation(receiver_id, invitation_id)
+
+ # (1) save to database
+ notification = Notification.new
+ notification.description = NotificationTypes::SESSION_INVITATION
+ notification.target_user_id = receiver_id
+ notification.save
+
+ # (2) create notification
+ msg = @@message_factory.session_invitation(receiver_id, invitation_id)
+
+ # (3) send notification
+ @@mq_router.publish_to_user(receiver_id, msg)
+ end
+
+ def send_musician_session_join(music_session, connection, user)
+
+ # (1) create notification
+ msg = @@message_factory.musician_session_join(music_session.id, user.id, user.name, user.photo_url)
+
+ # (2) send notification
+ @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => connection.client_id})
+ end
+
+ def send_musician_session_depart(music_session, client_id, user)
+
+ # (1) create notification
+ msg = @@message_factory.musician_session_depart(music_session.id, user.id, user.name, user.photo_url)
+
+ # (2) send notification
+ @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id})
+ end
+
+ def send_musician_session_fresh(music_session, client_id, user)
+
+ # (1) create notification
+ msg = @@message_factory.musician_session_fresh(music_session.id, user.id, user.name, user.photo_url)
+
+ # (2) send notification
+ @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id})
+ end
+
+ def send_musician_session_stale(music_session, client_id, user)
+
+ # (1) create notification
+ msg = @@message_factory.musician_session_stale(music_session.id, user.id, user.name, user.photo_url)
+
+ # (2) send notification
+ @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id})
+ end
+
+ def send_friend_session_join(db_conn, connection, user)
+ ids = retrieve_friends_and_followers_not_in_session(db_conn, user.id, connection.music_session.id)
+
+ if ids.length > 0
+ # (1) save to database
+
+ # (2) create notification
+ msg = @@message_factory.friend_session_join(connection.music_session.id, user.id, user.name, user.photo_url)
+
+ # (3) send notification
+ @@mq_router.publish_to_friends(ids, msg, sender = {:client_id => connection.client_id})
+ end
+ end
+
+ def send_join_request(music_session, join_request, sender, text)
+
+ # (1) save to database
+
+ # (2) create notification
+ msg = @@message_factory.join_request(music_session.id, join_request.id, sender.name, text)
+
+ # (3) send notification
+ @@mq_router.server_publish_to_session(music_session, msg)
+ end
+
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/recorded_track.rb b/ruby/lib/jam_ruby/models/recorded_track.rb
new file mode 100644
index 000000000..c53d02f62
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/recorded_track.rb
@@ -0,0 +1,83 @@
+module JamRuby
+ class RecordedTrack < ActiveRecord::Base
+
+ self.table_name = "recorded_tracks"
+
+ self.primary_key = 'id'
+
+ SOUND = %w(mono stereo)
+
+ belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :recorded_tracks
+ belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :recorded_tracks
+ belongs_to :instrument, :class_name => "JamRuby::Instrument"
+
+ validates :sound, :inclusion => {:in => SOUND}
+
+ before_destroy :delete_s3_files
+
+ # Copy an ephemeral track to create a saved one. Some fields are ok with defaults
+ def self.create_from_track(track, recording)
+ recorded_track = self.new
+ recorded_track.recording = recording
+ recorded_track.user = track.connection.user
+ recorded_track.instrument = track.instrument
+ recorded_track.sound = track.sound
+ recorded_track.save
+ recorded_track
+ end
+
+ def upload_start(length, md5)
+ self.upload_id = S3Manager.multipart_upload_start(hashed_filename)
+ self.length = length
+ self.md5 = md5
+ save
+ end
+
+ def upload_sign(content_md5)
+ S3Manager.upload_sign(hashed_filename, content_md5, upload_id)
+ end
+
+ def upload_part_complete(part)
+ raise JamRuby::JamArgumentError unless part == next_part_to_upload
+ self.next_part_to_upload = part + 1
+ save
+ end
+
+ def upload_complete
+ S3Manager.multipart_upload_complete(upload_id)
+ self.fully_uploaded = true
+ save
+ recording.upload_complete
+ end
+
+ def url
+ S3Manager.url(hashed_filename)
+ end
+
+ def filename
+ hashed_filename
+ end
+
+
+ # Format: "recording_#{recorded_track_id}"
+ # File extension is irrelevant actually.
+ def self.find_by_upload_filename(filename)
+ matches = /^recording_([\w-]+)$/.match(filename)
+ return nil unless matches && matches.length > 1
+ RecordedTrack.find(matches[1])
+ end
+
+
+ private
+
+ def delete_s3_files
+ S3Manager.delete(hashed_filename)
+ end
+
+ def hashed_filename
+ S3Manager.hashed_filename('recorded_track', id)
+ end
+
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb
new file mode 100644
index 000000000..d9e2a90ed
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/recording.rb
@@ -0,0 +1,281 @@
+module JamRuby
+ class Recording < ActiveRecord::Base
+
+ self.primary_key = 'id'
+
+ has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :recording
+ has_many :users, :through => :claimed_recordings, :class_name => "JamRuby::User"
+ belongs_to :owner, :class_name => "JamRuby::User", :inverse_of => :owned_recordings
+ belongs_to :band, :class_name => "JamRuby::Band", :inverse_of => :recordings
+ belongs_to :music_session, :class_name => "JamRuby::MusicSession", :inverse_of => :recording
+ has_many :mixes, :class_name => "JamRuby::Mix", :inverse_of => :recording
+ has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id
+
+
+ # Start recording a session.
+ def self.start(music_session_id, owner)
+
+ recording = nil
+
+ # Use a transaction and lock to avoid races.
+ ActiveRecord::Base.transaction do
+ music_session = MusicSession.find(music_session_id, :lock => true)
+
+ if music_session.nil?
+ raise PermissionError, "the session has ended"
+ end
+
+ unless music_session.recording.nil?
+ raise PermissionError, "the session is already being recorded"
+ end
+
+ recording = Recording.new
+ recording.music_session = music_session
+ recording.owner = owner
+
+ music_session.connections.each do |connection|
+ # Note that we do NOT connect the recording to any users at this point.
+ # That ONLY happens if a user clicks 'save'
+ # recording.users << connection.user
+ connection.tracks.each do |track|
+ RecordedTrack.create_from_track(track, recording)
+ end
+ end
+
+ # Note that I believe this can be nil.
+ recording.band = music_session.band
+ recording.save
+
+ music_session.recording = recording
+ music_session.save
+ end
+
+
+ # FIXME:
+ # NEED TO SEND NOTIFICATION TO ALL USERS IN THE SESSION THAT RECORDING HAS STARTED HERE.
+ # I'LL STUB IT A BIT. NOTE THAT I REDO THE FIND HERE BECAUSE I DON'T WANT TO SEND THESE
+ # NOTIFICATIONS WHILE THE DB ROW IS LOCKED
+ music_session = MusicSession.find(music_session_id)
+ music_session.connections.each do |connection|
+ # connection.notify_recording_has_started
+ end
+
+ recording
+ end
+
+ # Stop recording a session
+ def stop
+ # Use a transaction and lock to avoid races.
+ ActiveRecord::Base.transaction do
+ music_session = MusicSession.find(self.music_session_id, :lock => true)
+ if music_session.nil?
+ raise PermissionError, "the session has ended"
+ end
+ unless music_session.recording
+ raise PermissionError, "the session is not currently being recorded"
+ end
+ music_session.recording = nil
+ music_session.save
+ end
+
+ self.duration = Time.now - created_at
+ save
+ end
+
+
+ # Called when a user wants to "claim" a recording. To do this, the user must have been one of the tracks in the recording.
+ def claim(user, name, genre, is_public, is_downloadable)
+ if self.users.include?(user)
+ raise PermissionError, "user already claimed this recording"
+ end
+
+ unless self.recorded_tracks.find { |recorded_track| recorded_track.user == user }
+ raise PermissionError, "user was not in this session"
+ end
+
+ unless self.music_session.nil?
+ raise PermissionError, "recording cannot be claimed while it is being recorded"
+ end
+
+ if name.nil? || genre.nil? || is_public.nil? || is_downloadable.nil?
+ raise PermissionError, "recording must have name, genre and flags"
+ end
+
+ claimed_recording = ClaimedRecording.new
+ claimed_recording.user = user
+ claimed_recording.recording = self
+ claimed_recording.name = name
+ claimed_recording.genre = genre
+ claimed_recording.is_public = is_public
+ claimed_recording.is_downloadable = is_downloadable
+ self.claimed_recordings << claimed_recording
+ save
+
+ claimed_recording
+ end
+
+ # Find out if all the tracks for this recording have been uploaded
+ def uploaded?
+ self.recorded_tracks.each do |recorded_track|
+ return false unless recorded_track.fully_uploaded
+ end
+ return true
+ end
+
+ # Discards this recording and schedules deletion of all files associated with it.
+ def discard
+ self.destroy
+ end
+
+ # Returns the list of files the user needs to upload. This will only ever be recordings
+ def self.upload_file_list(user)
+ files = []
+ User.joins(:recordings).joins(:recordings => :recorded_tracks)
+ .where(%Q{ recordings.duration IS NOT NULL })
+ .where("recorded_tracks.user_id = '#{user.id}'")
+ .where(%Q{ recorded_tracks.fully_uploaded = FALSE }).each do |user|
+ user.recordings.each.do |recording|
+ recording.recorded_tracks.each do |recorded_track|
+ files.push(
+ {
+ :type => "recorded_track",
+ :id => recorded_track.id,
+ :url => recorded_track.url # FIXME IS THIS RIGHT?
+ }
+ )
+ end
+ end
+ files
+ end
+
+ # Returns the list of files this user should have synced to his computer, along with md5s and lengths
+ def self.list(user)
+ downloads = []
+
+ # That second join is important. It's saying join off of recordings, NOT user. If you take out the
+ # ":recordings =>" part, you'll just get the recorded_tracks that I played. Very different!
+ User.joins(:recordings).joins(:recordings => :recorded_tracks)
+ .order(%Q{ recordings.created_at DESC })
+ .where(%Q{ recorded_tracks.fully_uploaded = TRUE })
+ .where(:id => user.id).each do |theuser|
+ theuser.recordings.each do |recording|
+ recording.recorded_tracks.each do |recorded_track|
+ recorded_track = user.claimed_recordings.first.recording.recorded_tracks.first
+ downloads.push(
+ {
+ :type => "recorded_track",
+ :id => recorded_track.id,
+ :length => recorded_track.length,
+ :md5 => recorded_track.md5,
+ :url => recorded_track.url
+ }
+ )
+ end
+ end
+ end
+
+ User.joins(:recordings).joins(:recordings => :mixes)
+ .order(%Q{ recordings.created_at DESC })
+ .where(%Q{ mixes.completed_at IS NOT NULL }).each do |theuser|
+ theuser.recordings.each do |recording|
+ recording.mixes.each do |mix|
+ downloads.push(
+ {
+ :type => "mix",
+ :id => mix.id,
+ :length => mix.length,
+ :md5 => mix.md5,
+ :url => mix.url
+ }
+ )
+ end
+ end
+ end
+
+
+ uploads = []
+ RecordedTrack
+ .joins(:recording)
+ .where(:user_id => user.id)
+ .where(:fully_uploaded => false)
+ .where("duration IS NOT NULL").each do |recorded_track|
+ uploads.push(recorded_track.filename)
+ end
+
+ {
+ "downloads" => downloads,
+ "uploads" => uploads
+ }
+ end
+
+ # Check to see if all files have been uploaded. If so, kick off a mix.
+ def upload_complete
+ # Don't allow multiple mixes for now.
+ raise JamRuby::JamArgumentError unless self.mixes.length == 0
+
+ # FIXME: There's a possible race condition here. If two users complete
+ # uploads at the same time, we'll schedule 2 mixes.
+ recorded_tracks.each do |recorded_track|
+ return unless recorded_track.fully_uploaded
+ end
+
+ self.mixes << Mix.schedule(self, base_mix_manifest.to_json)
+
+ save
+ end
+
+=begin
+# This is no longer remotely right.
+ def self.search(query, options = { :limit => 10 })
+
+ # only issue search if at least 2 characters are specified
+ if query.nil? || query.length < 2
+ return []
+ end
+
+ # create 'anded' statement
+ query = Search.create_tsquery(query)
+
+ if query.nil? || query.length == 0
+ return []
+ end
+
+ return Recording.where("description_tsv @@ to_tsquery('jamenglish', ?)", query).limit(options[:limit])
+ end
+=end
+
+ def base_mix_manifest
+ manifest = { "files" => [], "timeline" => [] }
+ mix_params = []
+ recorded_tracks.each do |recorded_track|
+ return nil unless recorded_track.fully_uploaded
+ manifest["files"] << { "url" => recorded_track.url, "codec" => "vorbis", "offset" => 0 }
+ mix_params << { "level" => 100, "balance" => 0 }
+ end
+
+ manifest["timeline"] << { "timestamp" => 0, "mix" => mix_params }
+ manifest["timeline"] << { "timestamp" => duration, "end" => true }
+ manifest
+ end
+
+ private
+ def self.validate_user_is_band_member(user, band)
+ unless band.users.exists? user
+ raise PermissionError, ValidationMessages::USER_NOT_BAND_MEMBER_VALIDATION_ERROR
+ end
+ end
+
+ def self.validate_user_is_creator(user, creator)
+ unless user.id == creator.id
+ raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
+ end
+ end
+
+ def self.validate_user_is_musician(user)
+ unless user.musician?
+ raise PermissionError, ValidationMessages::USER_NOT_MUSICIAN_VALIDATION_ERROR
+ end
+ end
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/search.rb b/ruby/lib/jam_ruby/models/search.rb
new file mode 100644
index 000000000..c3c33c7e2
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/search.rb
@@ -0,0 +1,85 @@
+module JamRuby
+ # not a active_record model; just a search result
+ class Search
+ attr_accessor :bands, :musicians, :fans, :recordings, :friends
+
+ LIMIT = 10
+
+ # performs a site-white search
+ def self.search(query, user_id = nil)
+
+ users = User.search(query, :limit => LIMIT)
+ bands = Band.search(query, :limit => LIMIT)
+ # NOTE: I removed recordings from search here. This is because we switched
+ # to "claimed_recordings" so it's not clear what should be searched.
+
+ friends = Friendship.search(query, user_id, :limit => LIMIT)
+
+ return Search.new(users + bands + friends)
+ end
+
+ # performs a friend search scoped to a specific user
+ # def self.search_by_user(query, user_id)
+ # friends = Friendship.search(query, user_id, :limit => LIMIT)
+ # return Search.new(friends)
+ # end
+
+ # search_results - results from a Tire search across band/user/recording
+ def initialize(search_results)
+ @bands = []
+ @musicians = []
+ @fans = []
+ @recordings = []
+ @friends = []
+
+ if search_results.nil?
+ return
+ end
+
+ search_results.take(LIMIT).each do |result|
+ if result.class == User
+ if result.musician
+ @musicians.push(result)
+ else
+ @fans.push(result)
+ end
+ elsif result.class == Band
+ @bands.push(result)
+ elsif result.class == Recording
+ @recordings.push(result)
+ elsif result.class == Friendship
+ @friends.push(result.friend)
+ else
+ raise Exception, "unknown class #{result.class} returned in search results"
+ end
+ end
+ end
+
+ def self.create_tsquery(query)
+ # empty queries don't hit back to elasticsearch
+ if query.nil? || query.length == 0
+ return nil
+ end
+
+ search_terms = query.split
+
+ if search_terms.length == 0
+ return nil
+ end
+
+ args = nil
+ search_terms.each do |search_term|
+ if args == nil
+ args = search_term
+ else
+ args = args + " & " + search_term
+ end
+
+ end
+ args = args + ":*"
+
+ return args
+ end
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/track.rb b/ruby/lib/jam_ruby/models/track.rb
new file mode 100644
index 000000000..7ae02b24c
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/track.rb
@@ -0,0 +1,62 @@
+module JamRuby
+ class Track < ActiveRecord::Base
+
+ self.table_name = "tracks"
+
+ self.primary_key = 'id'
+
+ default_scope order('created_at ASC')
+
+ SOUND = %w(mono stereo)
+
+ belongs_to :connection, :class_name => "JamRuby::Connection", :inverse_of => :tracks
+ belongs_to :instrument, :class_name => "JamRuby::Instrument", :inverse_of => :tracks
+
+ validates :sound, :inclusion => {:in => SOUND}
+
+ def self.index(current_user, music_session_id)
+ query = Track
+ .joins(
+ %Q{
+ INNER JOIN
+ connections
+ ON
+ connections.music_session_id = '#{music_session_id}'
+ AND
+ connections.id = connection_id
+ AND
+ connections.user_id = '#{current_user.id}'
+ INNER JOIN
+ music_sessions
+ ON
+ music_sessions.id = '#{music_session_id}'
+ AND
+ music_sessions.user_id = '#{current_user.id}'
+ }
+ )
+
+ return query
+ end
+
+ def self.save(id, connection_id, instrument_id, sound)
+ if id.nil?
+ track = Track.new()
+ track.connection_id = connection_id
+ else
+ track = Track.find(id)
+ end
+
+ unless instrument_id.nil?
+ track.instrument_id = instrument_id
+ end
+
+ unless sound.nil?
+ track.sound = sound
+ end
+
+ track.updated_at = Time.now.getutc
+ track.save
+ return track
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb
new file mode 100644
index 000000000..bccb851d1
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/user.rb
@@ -0,0 +1,931 @@
+include Devise::Models
+
+module JamRuby
+ class User < ActiveRecord::Base
+
+ #devise: for later: :trackable
+
+ devise :database_authenticatable,
+ :recoverable, :rememberable
+
+
+ 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_s3_path, :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
+
+ # authorizations (for facebook, etc -- omniauth)
+ has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization"
+
+ # connections (websocket-gateway)
+ has_many :connections, :class_name => "JamRuby::Connection"
+
+ # friend requests
+ has_many :friend_requests, :class_name => "JamRuby::FriendRequest"
+
+ # instruments
+ has_many :musician_instruments, :class_name => "JamRuby::MusicianInstrument"
+ 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"
+
+ # recordings
+ has_many :owned_recordings, :class_name => "JamRuby::Recording"
+ has_many :recordings, :through => :claimed_recordings, :class_name => "JamRuby::Recording"
+ has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :user
+
+ # user likers (a musician has likers and may have likes too; fans do not have likers)
+ has_many :likers, :class_name => "JamRuby::UserLiker", :foreign_key => "user_id", :inverse_of => :user
+ has_many :inverse_likers, :through => :likers, :class_name => "JamRuby::User", :foreign_key => "liker_id"
+
+ # user likes (fans and musicians have likes)
+ has_many :likes, :class_name => "JamRuby::UserLike", :foreign_key => "liker_id", :inverse_of => :user
+ has_many :inverse_likes, :through => :followings, :class_name => "JamRuby::User", :foreign_key => "user_id"
+
+ # band likes
+ has_many :band_likes, :class_name => "JamRuby::BandLiker", :foreign_key => "liker_id", :inverse_of => :user
+ has_many :inverse_band_likes, :through => :band_likes, :class_name => "JamRuby::Band", :foreign_key => "band_id"
+
+ # followers
+ has_many :user_followers, :class_name => "JamRuby::UserFollower", :foreign_key => "user_id"
+ has_many :followers, :through => :user_followers, :class_name => "JamRuby::User"
+ has_many :inverse_user_followers, :through => :followers, :class_name => "JamRuby::UserFollower", :foreign_key => "follower_id"
+ has_many :inverse_followers, :through => :inverse_user_followers, :source => :user, :class_name => "JamRuby::User"
+
+ # user followings
+ has_many :user_followings, :class_name => "JamRuby::UserFollowing", :foreign_key => "follower_id"
+ has_many :followings, :through => :user_followings, :class_name => "JamRuby::User"
+ has_many :inverse_user_followings, :through => :followings, :class_name => "JamRuby::UserFollowing", :foreign_key => "user_id"
+ has_many :inverse_followings, :through => :inverse_user_followings, :source => :user, :class_name => "JamRuby::User"
+
+ # band followings
+ has_many :b_followings, :class_name => "JamRuby::BandFollowing", :foreign_key => "follower_id"
+ has_many :band_followings, :through => :b_followings, :class_name => "JamRuby::Band"
+ has_many :inverse_b_followings, :through => :band_followings, :class_name => "JamRuby::BandFollowing", :foreign_key => "band_id"
+ has_many :inverse_band_followings, :through => :inverse_band_followings, :source => :band, :class_name => "JamRuby::Band"
+
+ # notifications
+ has_many :notifications, :class_name => "JamRuby::Notification", :foreign_key => "target_user_id"
+ has_many :inverse_notifications, :through => :notifications, :class_name => "JamRuby::User"
+
+ # friends
+ has_many :friendships, :class_name => "JamRuby::Friendship", :foreign_key => "user_id"
+ has_many :friends, :through => :friendships, :class_name => "JamRuby::User"
+ has_many :inverse_friendships, :class_name => "JamRuby::Friendship", :foreign_key => "friend_id"
+ has_many :inverse_friends, :through => :inverse_friendships, :source => :user, :class_name => "JamRuby::User"
+
+ # connections / music sessions
+ has_many :created_music_sessions, :foreign_key => "user_id", :inverse_of => :user, :class_name => "JamRuby::MusicSession" # sessions *created* by the user
+ has_many :music_sessions, :through => :connections, :class_name => "JamRuby::MusicSession"
+
+ # invitations
+ has_many :received_invitations, :foreign_key => "receiver_id", :inverse_of => :receiver, :class_name => "JamRuby::Invitation"
+ has_many :sent_invitations, :foreign_key => "sender_id", :inverse_of => :sender, :class_name => "JamRuby::Invitation"
+
+ # fan invitations
+ has_many :received_fan_invitations, :foreign_key => "receiver_id", :inverse_of => :receiver, :class_name => "JamRuby::FanInvitation"
+ has_many :sent_fan_invitations, :foreign_key => "sender_id", :inverse_of => :sender, :class_name => "JamRuby::FanInvitation"
+
+ # band invitations
+ has_many :received_band_invitations, :inverse_of => :receiver, :foreign_key => "user_id", :class_name => "JamRuby::BandInvitation"
+ has_many :sent_band_invitations, :inverse_of => :sender, :foreign_key => "creator_id", :class_name => "JamRuby::BandInvitation"
+
+ # session history
+ has_many :music_session_histories, :foreign_key => "user_id", :class_name => "JamRuby::MusicSessionHistory", :inverse_of => :user
+ has_many :music_session_user_histories, :foreign_key => "user_id", :class_name => "JamRuby::MusicSessionUserHistory", :inverse_of => :user
+
+ # saved tracks
+ has_many :recorded_tracks, :foreign_key => "user_id", :class_name => "JamRuby::RecordedTrack", :inverse_of => :user
+
+ # invited users
+ has_many :invited_users, :foreign_key => "sender_id", :class_name => "JamRuby::InvitedUser"
+
+ # crash dumps
+ has_many :crash_dumps, :foreign_key => "user_id", :class_name => "JamRuby::CrashDump"
+
+ # This causes the authenticate method to be generated (among other stuff)
+ #has_secure_password
+
+ before_save :create_remember_token, :if => :should_validate_password?
+ 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
+ VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
+ validates :email, presence: true, format: {with: VALID_EMAIL_REGEX}
+ validates :update_email, presence: true, format: {with: VALID_EMAIL_REGEX}, :if => :updating_email
+
+ validates_length_of :password, minimum: 6, maximum: 100, :if => :should_validate_password?
+ 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 :subscribe_email, :inclusion => {:in => [nil, true, false]}
+ validates :musician, :inclusion => {:in => [true, false]}
+
+ # custom validators
+ validate :validate_musician_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
+
+ def validate_musician_instruments
+ errors.add(:musician_instruments, ValidationMessages::INSTRUMENT_MINIMUM_NOT_MET) if !administratively_created && musician && musician_instruments.length == 0
+ errors.add(:musician_instruments, ValidationMessages::INSTRUMENT_LIMIT_EXCEEDED) if !administratively_created && musician && musician_instruments.length > 5
+ 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)
+ end
+
+ def validate_update_email
+ if updating_email && self.update_email == self.email
+ errors.add(:update_email, ValidationMessages::EMAIL_MATCHES_CURRENT)
+ elsif updating_email && User.where("email ILIKE ?", self.update_email).first != nil
+ errors.add(:update_email, ValidationMessages::EMAIL_ALREADY_TAKEN)
+ end
+ 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?
+ end
+ end
+
+ def email_case_insensitive_uniqueness
+ # using the case insensitive unique check of active record will downcase the field, which is not what we want--we want to preserve original casing
+ search = User.where("email ILIKE ?", self.email).first
+ if search != nil && search != self
+ errors.add(:email, ValidationMessages::EMAIL_ALREADY_TAKEN)
+ end
+ end
+
+ def update_email_case_insensitive_uniqueness
+ # using the case insensitive unique check of active record will downcase the field, which is not what we want--we want to preserve original casing
+ search = User.where("update_email ILIKE ?", self.update_email).first
+ if search != nil && search != self
+ errors.add(:update_email, ValidationMessages::EMAIL_ALREADY_TAKEN)
+ end
+ end
+
+ def online
+ @online ||= !self.connections.nil? && self.connections.size > 0
+ end
+
+ def name
+ return "#{first_name} #{last_name}"
+ end
+
+ def location
+ 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?
+ loc
+ end
+
+ def location= location_hash
+ unless location_hash.blank?
+ self.city = location_hash[:city]
+ self.state = location_hash[:state]
+ self.country = location_hash[:country]
+ end if self.city.blank?
+ end
+
+ def musician?
+ return musician
+ end
+
+ def should_validate_password?
+ (updating_password || new_record?)
+ end
+
+ def should_confirm_existing_password?
+ confirm_current_password
+ end
+
+ def end_user_created?
+ return !administratively_created
+ end
+
+ def friends?(user)
+ return self.friends.exists?(user)
+ end
+
+ def friend_count
+ return self.friends.size
+ end
+
+ def liker_count
+ return self.likers.size
+ end
+
+ def like_count
+ return self.likes.size
+ end
+
+ def band_like_count
+ return self.band_likes.size
+ end
+
+ def follower_count
+ return self.followers.size
+ end
+
+ def following_count
+ return self.followings.size
+ end
+
+ def band_following_count
+ return self.band_followings.size
+ end
+
+ def recording_count
+ return self.recordings.size
+ end
+
+ def session_count
+ return self.music_sessions.size
+ end
+
+ def confirm_email!
+ self.email_confirmed = true
+ end
+
+ def my_session_settings
+ unless self.session_settings.nil?
+ return JSON.parse(self.session_settings)
+ else
+ return ""
+ end
+ end
+
+ def session_history(user_id, band_id = nil, genre = nil)
+ return MusicSessionHistory.index(self, user_id, band_id, genre)
+ end
+
+ def session_user_history(user_id, session_id)
+ return MusicSessionUserHistory.where("music_session_id='#{session_id}'")
+ end
+
+ # always returns a non-null value for photo-url,
+ # using the generic avatar if no user photo available
+ def resolved_photo_url
+ if self.photo_url == nil || self.photo_url == ''
+ # lame that this isn't environment, but boy this is hard to pass all the way down from jam-web!
+ "http://www.jamkazam.com/assets/shared/avatar_generic.png"
+ else
+ return self.photo_url
+ end
+ end
+
+ def to_s
+ return email unless email.nil?
+
+ if !first_name.nil? && !last_name.nil?
+ return first_name + ' ' + last_name
+ end
+
+ return id
+ end
+
+ def set_password(old_password, new_password, new_password_confirmation)
+
+ # so that UserObserver knows to send a confirmation email on success
+ self.setting_password = true
+ # so that should_validate_password? fires
+ self.updating_password = true
+
+ 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)
+ else
+ self.assign_attributes(attributes)
+ self.valid?
+ self.errors.add(:current_password, old_password.blank? ? :blank : :invalid)
+ end
+
+ #clean_up_passwords
+
+ end
+
+ def self.set_password_from_token(email, token, new_password, new_password_confirmation)
+ user = User.where("email ILIKE ?", email).first
+ if user.nil? || user.reset_password_token != token || Time.now - user.reset_password_token_created > 3.days || new_password.length < 6 || new_password != new_password_confirmation
+ raise JamRuby::JamArgumentError
+ end
+ user.reset_password_token = nil
+ user.reset_password_token_created = nil
+ 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.password = new_password
+ self.password_confirmation = new_password_confirmation
+
+ UserMailer.password_changed(self).deliver
+ end
+
+ def self.reset_password(email, base_uri)
+ user = User.where("email ILIKE ?", email).first
+ raise JamRuby::JamArgumentError if user.nil?
+
+ user.reset_password_token = SecureRandom.urlsafe_base64
+ user.reset_password_token_created = Time.now
+ user.save
+
+ reset_url = "#{base_uri}/reset_password_token?token=#{user.reset_password_token}&email=#{CGI.escape(email)}"
+ UserMailer.password_reset(user, reset_url).deliver
+
+ user
+ end
+
+ def self.band_index(user_id)
+ bands = Band.joins(:band_musicians)
+ .where(:bands_musicians => {:user_id => "#{user_id}"})
+
+ return bands
+ end
+
+ def self.recording_index(current_user, user_id)
+ hide_private = false
+
+ # hide private recordings from anyone but the current user
+ if current_user.id != user_id
+ hide_private = true
+ end
+
+ if hide_private
+ recordings = Recording.joins(:musician_recordings)
+ .where(:musicians_recordings => {:user_id => "#{user_id}"}, :public => true)
+
+ else
+ recordings = Recording.joins(:musician_recordings)
+ .where(:musicians_recordings => {:user_id => "#{user_id}"})
+ end
+
+ return recordings
+ end
+
+ # given an array of instruments, update a user's instruments
+ def update_instruments(instruments)
+ # delete all instruments for this user first
+ unless self.new_record?
+ MusicianInstrument.delete_all(["user_id = ?", self.id])
+ end
+
+ # loop through each instrument in the array and save to the db
+ instruments.each do |musician_instrument_param|
+ instrument = Instrument.find(musician_instrument_param[:instrument_id])
+ musician_instrument = MusicianInstrument.new
+ musician_instrument.user = self
+ musician_instrument.instrument = instrument
+ musician_instrument.proficiency_level = musician_instrument_param[:proficiency_level]
+ musician_instrument.priority = musician_instrument_param[:priority]
+ musician_instrument.save
+ self.musician_instruments << musician_instrument
+ end
+ end
+
+ # 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)
+
+ # first name
+ unless first_name.nil?
+ self.first_name = first_name
+ end
+
+ # last name
+ unless last_name.nil?
+ self.last_name = last_name
+ end
+
+ # email
+ # !! Email is changed in a dedicated method, 'update_email'
+ #unless email.nil?
+ # self.email = email
+ #end
+
+ # password
+ unless password.nil?
+ self.password = password
+ end
+
+ # password confirmation
+ unless password_confirmation.nil?
+ self.password_confirmation = password_confirmation
+ end
+
+ # musician flag
+ unless musician.nil?
+ self.musician = musician
+ end
+
+ # gender
+ unless gender.nil?
+ self.gender = gender
+ end
+
+ # birthdate
+ unless birth_date.nil?
+ self.birth_date = birth_date
+ end
+
+ # ISP
+ unless internet_service_provider.nil?
+ self.internet_service_provider = internet_service_provider
+ end
+
+ # city
+ unless city.nil?
+ self.city = city
+ end
+
+ # state
+ unless state.nil?
+ self.state = state
+ end
+
+ # country
+ unless country.nil?
+ self.country = country
+ end
+
+ # instruments
+ unless instruments.nil?
+ update_instruments(instruments)
+ end
+
+ # photo url
+ unless photo_url.nil?
+ self.photo_url = photo_url
+ end
+
+ self.updated_at = Time.now.getutc
+ self.save
+ end
+
+ # 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)
+ if id.nil?
+ user = User.new()
+ else
+ user = User.find(id)
+ end
+
+ if user.id != updater_id
+ raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
+ 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)
+ return user
+ end
+
+ def begin_update_email(email, current_password, confirmation_url)
+ # sets the user model in a state such that it's expecting to have it's email updated
+ # two columns matter for this; 'update_email_token' and 'update_email'
+ # confirmation_link is odd in the sense that it can likely only come from www.jamkazam.com (jam-web)
+
+ # an observer should be set up to send an email based on this activity
+ self.updating_email = self.confirm_current_password = true
+ self.current_password = current_password
+ self.update_email = email
+ self.update_email_token = SecureRandom.urlsafe_base64
+ self.update_email_confirmation_url = "#{confirmation_url}#{self.update_email_token}"
+
+ self.save
+ end
+
+ def self.finalize_update_email(update_email_token)
+ # updates the user model to have a new email address
+ user = User.find_by_update_email_token!(update_email_token)
+
+ user.updated_email = true
+ user.email = user.update_email
+ user.update_email_token = nil
+ user.save
+
+ return user
+ end
+
+
+ def self.create_user_like(user_id, liker_id)
+ liker = UserLiker.new()
+ liker.user_id = user_id
+ liker.liker_id = liker_id
+ liker.save
+ end
+
+ def self.delete_like(user_id, band_id, liker_id)
+ if !user_id.nil?
+ JamRuby::UserLiker.delete_all "(user_id = '#{user_id}' AND liker_id = '#{liker_id}')"
+
+ elsif !band_id.nil?
+ JamRuby::BandLiker.delete_all "(band_id = '#{band_id}' AND liker_id = '#{liker_id}')"
+ end
+ end
+
+ def self.create_band_like(band_id, liker_id)
+ liker = BandLiker.new()
+ liker.band_id = band_id
+ liker.liker_id = liker_id
+ liker.save
+ end
+
+ def self.delete_band_like(band_id, liker_id)
+ JamRuby::BandLiker.delete_all "(band_id = '#{band_id}' AND liker_id = '#{liker_id}')"
+ end
+
+ def self.create_user_following(user_id, follower_id)
+ follower = UserFollower.new()
+ follower.user_id = user_id
+ follower.follower_id = follower_id
+ follower.save
+ end
+
+ def self.delete_following(user_id, band_id, follower_id)
+ if !user_id.nil?
+ JamRuby::UserFollower.delete_all "(user_id = '#{user_id}' AND follower_id = '#{follower_id}')"
+
+ elsif !band_id.nil?
+ JamRuby::BandFollower.delete_all "(band_id = '#{band_id}' AND follower_id = '#{follower_id}')"
+ end
+ end
+
+ def self.create_band_following(band_id, follower_id)
+ follower = BandFollower.new()
+ follower.band_id = band_id
+ follower.follower_id = follower_id
+ follower.save
+ end
+
+ def self.delete_band_following(band_id, follower_id)
+ JamRuby::BandFollower.delete_all "(band_id = '#{band_id}' AND follower_id = '#{follower_id}')"
+ end
+
+ def self.create_favorite(user_id, recording_id)
+ favorite = UserFavorite.new()
+ favorite.user_id = user_id
+ favorite.recording_id = recording_id
+ favorite.save
+ end
+
+ def self.delete_favorite(user_id, recording_id)
+ JamRuby::UserFavorite.delete_all "(user_id = '#{user_id}' AND recording_id = '#{recording_id}')"
+ end
+
+ def self.save_session_settings(user, music_session)
+ unless user.nil?
+
+ # only save genre id and description
+ genres = []
+ unless music_session.genres.nil?
+ music_session.genres.each do |genre|
+ g = Hash.new
+ g["id"] = genre.id
+ g["description"] = genre.description
+ genres << g
+ end
+ end
+
+ # only save invitation receiver id and name
+ invitees = []
+ unless music_session.invitations.nil?
+ music_session.invitations.each do |invitation|
+ i = Hash.new
+ i["id"] = invitation.receiver.id
+ i["name"] = invitation.receiver.name
+ invitees << i
+ end
+ end
+
+ session_settings = { :band_id => music_session.band_id,
+ :musician_access => music_session.musician_access,
+ :approval_required => music_session.approval_required,
+ :fan_chat => music_session.fan_chat,
+ :fan_access => music_session.fan_access,
+ :description => music_session.description,
+ :genres => genres,
+ :invitees => invitees
+ }.to_json
+
+ user.session_settings = session_settings
+ user.save
+ end
+ end
+
+ # throws ActiveRecord::RecordNotFound if instrument is invalid
+ # throws an email delivery error if unable to connect out to SMTP
+ def self.signup(first_name, last_name, email, password, password_confirmation, terms_of_service, subscribe_email,
+ location, instruments, birth_date, musician, photo_url, invited_user, signup_confirm_url)
+ user = User.new
+
+ UserManager.active_record_transaction do |user_manager|
+ user.first_name = first_name
+ user.last_name = last_name
+ user.email = email
+ user.subscribe_email = subscribe_email
+ user.terms_of_service = terms_of_service
+ user.musician = musician
+
+ # FIXME: Setting random password for social network logins. This
+ # is because we have validations all over the place on this.
+ # The right thing would be to have this null
+
+ # 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
+ else
+ user.password = password
+ user.password_confirmation = password_confirmation
+ end
+
+ user.admin = false
+ user.city = location[:city]
+ user.state = location[:state]
+ user.country = location[:country]
+ user.birth_date = birth_date
+
+ if user.musician # only update instruments if the user is a musician
+ unless instruments.nil?
+ instruments.each do |musician_instrument_param|
+ instrument = Instrument.find(musician_instrument_param[:instrument_id])
+ musician_instrument = MusicianInstrument.new
+ musician_instrument.user = user
+ musician_instrument.instrument = instrument
+ musician_instrument.proficiency_level = musician_instrument_param[:proficiency_level]
+ musician_instrument.priority = musician_instrument_param[:priority]
+ user.musician_instruments << musician_instrument
+ end
+ end
+ end
+
+ user.photo_url = photo_url
+
+
+ if invited_user.nil?
+ user.can_invite = Limits::USERS_CAN_INVITE
+ user.email_confirmed = false
+ user.signup_token = SecureRandom.urlsafe_base64
+ else
+ # if you are invited by an admin, we'll say you can invite too.
+ # but if not, then you can not invite
+ user.can_invite = invited_user.invited_by_administrator?
+
+ # if you came in from an invite and used the same email to signup,
+ # then we know you are a real human and that your email is valid.
+ # lucky! we'll log you in immediately
+ if invited_user.email.casecmp(user.email).zero?
+ user.email_confirmed = true
+ user.signup_token = nil
+ else
+ user.email_confirmed = false
+ user.signup_token = SecureRandom.urlsafe_base64
+ end
+
+
+ # now that the user is saved, let's
+ if invited_user.autofriend && !invited_user.sender.nil?
+ # hookup this user with the sender
+ Friendship.save_using_models(user, invited_user.sender)
+ end
+
+ invited_user.accept!
+ invited_user.save
+
+ if invited_user.errors.any?
+ raise ActiveRecord::Rollback
+ end
+ end
+
+ user.save
+
+ if user.errors.any?
+ raise ActiveRecord::Rollback
+ else
+ # don't send an signup email if the user was invited already *and* they used the same email that they were invited with
+ if !invited_user.nil? && invited_user.email.casecmp(user.email).zero?
+ else
+
+ # FIXME:
+ # It's not standard to require a confirmation when a user signs up with Facebook.
+ # We should stop asking for it.
+ #
+ # 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.welcome_message(user, signup_confirm_url.nil? ? nil : (signup_confirm_url + "/" + user.signup_token) ).deliver
+ end
+ end
+ end
+
+ return user
+ end
+
+ # 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
+ # of constantly recreating users as we create new dev environments
+
+ # We guard against this code running in production mode,
+ # 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)
+
+ if Environment.mode == "production"
+ # short-circuit out
+ return
+ end
+
+ user = User.find_or_create_by_email(email)
+
+ User.transaction do
+ user.first_name = first_name
+ user.last_name = last_name
+ user.email = email
+ user.password = password
+ user.password_confirmation = password
+ user.admin = true
+ user.email_confirmed = true
+ user.musician = true
+ user.city = city
+ user.state = state
+ user.country = country
+ user.terms_of_service = true
+
+ if instruments.nil?
+ instruments = [{:instrument_id => "acoustic guitar", :proficiency_level => 3, :priority => 1}]
+ end
+
+ unless user.new_record?
+ MusicianInstrument.delete_all(["user_id = ?", user.id])
+ end
+
+ instruments.each do |musician_instrument_param|
+ instrument = Instrument.find(musician_instrument_param[:instrument_id])
+ musician_instrument = MusicianInstrument.new
+ musician_instrument.user = user
+ musician_instrument.instrument = instrument
+ musician_instrument.proficiency_level = musician_instrument_param[:proficiency_level]
+ musician_instrument.priority = musician_instrument_param[:priority]
+ user.musician_instruments << musician_instrument
+ end
+
+ if photo_url.nil?
+ user.photo_url = photo_url
+ end
+
+ user.signup_token = nil
+ user.save
+
+ if user.errors.any?
+ raise ActiveRecord::Rollback
+ end
+ end
+
+ return user
+ end
+
+ def signup_confirm
+ self.signup_token = nil
+ self.confirm_email!
+ self.save
+ end
+
+ def update_avatar(original_fpfile, cropped_fpfile, crop_selection, aws_bucket)
+ self.updating_avatar = true
+
+ cropped_s3_path = cropped_fpfile["key"]
+
+ return self.update_attributes(
+ :original_fpfile => original_fpfile,
+ :cropped_fpfile => cropped_fpfile,
+ :cropped_s3_path => cropped_s3_path,
+ :crop_selection => crop_selection,
+ :photo_url => S3Util.url(aws_bucket, cropped_s3_path, :secure => false)
+ )
+ 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)
+ end
+
+ return self.update_attributes(
+ :original_fpfile => nil,
+ :cropped_fpfile => nil,
+ :cropped_s3_path => nil,
+ :photo_url => nil,
+ :crop_selection => nil
+ )
+ end
+
+ end
+
+ # throws RecordNotFound if signup token is invalid; i.e., if it's nil, empty string, or not belonging to a user
+ def self.signup_confirm(signup_token)
+ if signup_token.nil? || signup_token.empty?
+ # 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|
+ # throws ActiveRecord::RecordNotFound if invalid
+ user = User.find_by_signup_token!(signup_token)
+ user.signup_confirm
+ return user
+ end
+ end
+ end
+
+ # if valid credentials are supplied for an 'active' user, returns the user
+ # if not authenticated, returns nil
+ def self.authenticate(email, password)
+ # remove email_confirmed restriction due to VRFS-378
+
+ # we only allow users that have confirmed email to authenticate
+ # user = User.where('email_confirmed=true').find_by_email(email)
+
+ # do a case insensitive search for email, because we store it case sensitive
+ user = User.where("email ILIKE ?", email).first
+
+ if user && user.valid_password?(password)
+ return user
+ else
+ return nil
+ end
+ end
+
+ def self.search(query, options = { :limit => 10 })
+
+ # only issue search if at least 2 characters are specified
+ if query.nil? || query.length < 2
+ return []
+ end
+
+ # save query for use in instrument search
+ search_criteria = query
+
+ # create 'anded' statement
+ query = Search.create_tsquery(query)
+
+ if query.nil? || query.length == 0
+ return []
+ end
+
+ # remove email_confirmed restriction due to VRFS-378
+ # .where("email_confirmed = true AND (name_tsv @@ to_tsquery('jamenglish', ?) OR users.id in (select user_id from musicians_instruments where instrument_id like '%#{search_criteria.downcase}%'))", query)
+
+ return query = User
+ .where("(name_tsv @@ to_tsquery('jamenglish', ?) OR users.id in (select user_id from musicians_instruments where instrument_id like '%#{search_criteria.downcase}%'))", query)
+ .limit(options[:limit])
+ end
+
+ # devise compatibility
+
+ #def encrypted_password
+ # logger.debug("password digest returned #{self.password_digest}")
+ # self.password_digest
+ #end
+
+ #def encrypted_password=(encrypted_password)
+ # self.password_digest = encrypted_password
+ #end
+
+ # end devise compatibility
+ 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?
+ end
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/user_authorization.rb b/ruby/lib/jam_ruby/models/user_authorization.rb
new file mode 100644
index 000000000..5f7c97e25
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/user_authorization.rb
@@ -0,0 +1,16 @@
+module JamRuby
+ class UserAuthorization < ActiveRecord::Base
+
+ attr_accessible :provider, :uid, :token, :token_expiration
+
+ self.table_name = "user_authorizations"
+
+ self.primary_key = 'id'
+
+ belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id"
+ validates :provider, :uid, :presence => true
+
+ # token and token_expiration can be missing
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/user_follower.rb b/ruby/lib/jam_ruby/models/user_follower.rb
new file mode 100644
index 000000000..e3cd4615a
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/user_follower.rb
@@ -0,0 +1,11 @@
+module JamRuby
+ class UserFollower < ActiveRecord::Base
+
+ self.table_name = "users_followers"
+
+ self.primary_key = 'id'
+
+ belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id", :inverse_of => :inverse_followers
+ belongs_to :follower, :class_name => "JamRuby::User", :foreign_key => "follower_id", :inverse_of => :followers
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/user_following.rb b/ruby/lib/jam_ruby/models/user_following.rb
new file mode 100644
index 000000000..ea9f99ab8
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/user_following.rb
@@ -0,0 +1,11 @@
+module JamRuby
+ class UserFollowing < ActiveRecord::Base
+
+ self.table_name = "users_followers"
+
+ self.primary_key = 'id'
+
+ belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "follower_id", :inverse_of => :inverse_followings
+ belongs_to :following, :class_name => "JamRuby::User", :foreign_key => "user_id", :inverse_of => :followings
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/user_like.rb b/ruby/lib/jam_ruby/models/user_like.rb
new file mode 100644
index 000000000..915ab4d87
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/user_like.rb
@@ -0,0 +1,10 @@
+module JamRuby
+ class UserLike < ActiveRecord::Base
+
+ self.table_name = "users_likers"
+
+ self.primary_key = 'id'
+
+ belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id", :inverse_of => :inverse_likes
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/user_liker.rb b/ruby/lib/jam_ruby/models/user_liker.rb
new file mode 100644
index 000000000..07c2cbec7
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/user_liker.rb
@@ -0,0 +1,10 @@
+module JamRuby
+ class UserLiker < ActiveRecord::Base
+
+ self.table_name = "users_likers"
+
+ self.primary_key = 'id'
+
+ belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "liker_id", :inverse_of => :inverse_likers
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/user_observer.rb b/ruby/lib/jam_ruby/models/user_observer.rb
new file mode 100644
index 000000000..5a031340a
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/user_observer.rb
@@ -0,0 +1,16 @@
+module JamRuby
+ class UserObserver < ActiveRecord::Observer
+
+ observe JamRuby::User
+
+ def after_save(user)
+ if user.updating_email && !user.errors.any?
+ UserMailer.updating_email(user).deliver
+ elsif user.updated_email && !user.errors.any?
+ UserMailer.updated_email(user).deliver
+ elsif user.setting_password && !user.errors.any?
+ UserMailer.password_changed(user).deliver
+ end
+ 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
new file mode 100644
index 000000000..8fb3b30a6
--- /dev/null
+++ b/ruby/lib/jam_ruby/mq_router.rb
@@ -0,0 +1,98 @@
+require 'eventmachine'
+
+class MQRouter
+
+ # monostate pattern:
+ # You can initialize MQRouter instances as you want,
+ # but ultimately there are internal static state variables to represent global MQ exchange connections
+
+ class << self
+ attr_accessor :client_exchange, :user_exchange
+ @@log = Logging.logger[MQRouter]
+ end
+
+ def access_music_session(music_session, user)
+ if music_session.nil?
+ raise ArgumentError, 'specified session not found'
+ end
+
+ if !music_session.access? user
+ raise PermissionError, 'not allowed to join the specified session'
+ end
+
+ return music_session
+ end
+
+ # sends a message to a session on behalf of a user
+ # if this is originating in the context of a client, it should be specified as :client_id => "value"
+ # client_msg should be a well-structure message (jam-pb message)
+ def user_publish_to_session(music_session, user, client_msg, sender = {:client_id => ""})
+ access_music_session(music_session, user)
+
+ # gather up client_ids in the session
+ client_ids = music_session.connections.map { |client| client.client_id }.reject { |client_id| client_id == sender[:client_id] }
+
+ publish_to_session(music_session.id, client_ids, client_msg.to_s, sender)
+ end
+
+ # sends a message to a session from the server
+ # no access check as with user_publish_to_session
+ # client_msg should be a well-structure message (jam-pb message)
+ def server_publish_to_session(music_session, client_msg, sender = {:client_id => ""})
+ # gather up client_ids in the session
+ client_ids = music_session.connections.map { |client| client.client_id }.reject { |client_id| client_id == sender[:client_id] }
+
+ publish_to_session(music_session.id, client_ids, client_msg.to_s, sender)
+ end
+
+ # sends a message to a client with no checking of permissions (RAW USAGE)
+ # this method deliberately has no database interactivity/active_record objects
+ def publish_to_client(client_id, client_msg, sender = {:client_id => ""})
+ EM.schedule do
+ sender_client_id = sender[:client_id]
+
+ @@log.debug "publishing to client:#{client_id} from client:#{sender_client_id}"
+ # put it on the topic exchange for clients
+ self.class.client_exchange.publish(client_msg, :routing_key => "client.#{client_id}")
+ end
+ 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 => ""})
+ EM.schedule do
+ sender_client_id = sender[:client_id]
+
+ # iterate over each person in the session, and send a p2p message
+ client_ids.each do |client_id|
+
+ @@log.debug "publishing to session:#{music_session_id} / client:#{client_id} from client:#{sender_client_id}"
+ # put it on the topic exchange for clients
+ self.class.client_exchange.publish(client_msg, :routing_key => "client.#{client_id}")
+ end
+ end
+ end
+
+ # 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)
+ EM.schedule do
+ @@log.debug "publishing to user:#{user_id} from server"
+ # put it on the topic exchange for users
+ self.class.user_exchange.publish(user_msg, :routing_key => "user.#{user_id}")
+ end
+ end
+
+ # sends a message to a list of friends with no checking of permissions (RAW USAGE)
+ # this method deliberately has no database interactivity/active_record objects
+ def publish_to_friends(friend_ids, user_msg, from_user_id)
+ EM.schedule do
+ friend_ids.each do |friend_id|
+ @@log.debug "publishing to friend:#{friend_id} from user #{from_user_id}"
+ # put it on the topic exchange for users
+ self.class.user_exchange.publish(user_msg, :routing_key => "user.#{friend_id}")
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/version.rb b/ruby/lib/jam_ruby/version.rb
new file mode 100644
index 000000000..33161c160
--- /dev/null
+++ b/ruby/lib/jam_ruby/version.rb
@@ -0,0 +1,3 @@
+module JamRuby
+ VERSION = "0.0.1"
+end
diff --git a/ruby/migrate.sh b/ruby/migrate.sh
new file mode 100755
index 000000000..a9afa1578
--- /dev/null
+++ b/ruby/migrate.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+bundle exec jam_db up --connopts=dbname:jam host:localhost user:postgres password:postgres --verbose
diff --git a/ruby/scripts/simple_amqp_manager.rb b/ruby/scripts/simple_amqp_manager.rb
new file mode 100644
index 000000000..3c898b930
--- /dev/null
+++ b/ruby/scripts/simple_amqp_manager.rb
@@ -0,0 +1,78 @@
+############
+# USAGE
+############
+#
+# jam-ruby$> bundle exec ruby scripts/simple_amqp_manager.rb
+
+############
+# OVERVIEW
+############
+#
+# This is a simple user of AmqpConnectionManager (a jam-ruby class), which will continually
+# send messages to rabbitmq, and, if it receives them, print them.
+#
+
+############
+# TESTS
+############
+#
+# Test 1: start with rabbitmq down
+# ------
+# * stop rabbitmq
+# * run this file
+# * start rabbitmq, and messages should be sent/received
+#
+# Test 2: restart rabbitmq at steady state
+# ------
+# * start rabbitmq
+# * run this file
+# * messages should be sent/received
+# * restart rabbitmq
+# * once rabbitmq is back up, messages should be sent/received
+#
+
+require 'amqp'
+require 'active_record'
+require 'jam_db'
+
+# initialize ActiveRecord's db connection
+ActiveRecord::Base.establish_connection(YAML::load(File.open('config/database.yml'))["test"])
+
+require 'jam_ruby'
+
+# initialize logging
+Logging.logger.root.level = :debug
+Logging.logger.root.appenders = Logging.appenders.stdout
+
+
+log = Logging.logger['SimpleAmqpManager']
+
+
+include JamRuby
+
+users_exchange = nil
+
+EventMachine.run do
+
+ manager = AmqpConnectionManager.new(true, 4, :host => '127.0.0.1', :port => 5672)
+ manager.connect do |channel|
+ log.debug "initializing channel with registration to dog topic"
+
+ users_exchange = channel.topic('dogs')
+ # create user messaging topic
+ user_topic = channel.queue("", :auto_delete => true)
+ user_topic.bind(users_exchange, :routing_key => "dog.#")
+ user_topic.purge
+
+ user_topic.subscribe(:ack => false) do |headers, msg|
+ log.debug("received message from dog queue: #{msg}")
+ end
+ end
+
+ EventMachine.add_periodic_timer(2) do
+ unless users_exchange.nil? # if we have not connected yet ever, this will be nil
+ log.debug "sending message: [super secret message]"
+ users_exchange.publish("[super secret message]", :routing_key => "dog.leg")
+ end
+ end
+end
diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb
new file mode 100644
index 000000000..951cd7232
--- /dev/null
+++ b/ruby/spec/factories.rb
@@ -0,0 +1,124 @@
+FactoryGirl.define do
+ factory :user, :class => JamRuby::User do
+ 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
+ city "Apex"
+ state "NC"
+ country "USA"
+ musician true
+ terms_of_service true
+
+ #u.association :musician_instrument, factory: :musician_instrument, user: u
+
+ before(:create) do |user|
+ user.musician_instruments << FactoryGirl.build(:musician_instrument, user: user)
+ end
+
+ factory :admin do
+ admin true
+ end
+ end
+
+ factory :music_session, :class => JamRuby::MusicSession do
+ sequence(:description) { |n| "Music Session #{n}" }
+ fan_chat true
+ fan_access true
+ approval_required false
+ musician_access true
+ legal_terms true
+ association :creator, :factory => :user
+ end
+
+ factory :music_session_history, :class => JamRuby::MusicSessionHistory do
+ ignore do
+ music_session nil
+ end
+
+ music_session_id { music_session.id }
+ description { music_session.description }
+ user_id { music_session.user_id }
+ band_id { music_session.band_id }
+ end
+
+ factory :music_session_user_history, :class => JamRuby::MusicSessionUserHistory do
+ ignore do
+ history nil
+ user nil
+ end
+
+ music_session_id { history.music_session_id }
+ user_id { user.id }
+ sequence(:client_id) { |n| "Connection #{n}" }
+ end
+
+ factory :connection, :class => JamRuby::Connection do
+ sequence(:client_id) { |n| "Client#{n}" }
+ as_musician true
+ end
+
+ factory :invitation, :class => JamRuby::Invitation do
+
+ end
+
+ factory :friendship, :class => JamRuby::Friendship do
+
+ end
+
+ factory :band_musician, :class => JamRuby::BandMusician do
+
+ end
+
+ factory :band, :class => JamRuby::Band do
+ sequence(:name) { |n| "Band" }
+ biography "My Biography"
+ city "Apex"
+ state "NC"
+ country "USA"
+ end
+
+ factory :genre, :class => JamRuby::Genre do
+ description { |n| "Genre #{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"
+
+ end
+
+ factory :recorded_track, :class => JamRuby::RecordedTrack do
+ end
+
+ factory :instrument, :class => JamRuby::Instrument do
+
+ end
+
+ factory :recording, :class => JamRuby::Recording do
+
+ end
+
+ factory :musician_instrument, :class => JamRuby::MusicianInstrument do
+ instrument { Instrument.find('electric guitar') }
+ proficiency_level 1
+ priority 0
+ end
+
+ factory :invited_user, :class => JamRuby::InvitedUser do
+ sequence(:email) { |n| "user#{n}@someservice.com" }
+ autofriend false
+ end
+
+ factory :music_session_perf_data, :class => JamRuby::MusicSessionPerfData do
+ association :music_session => :music_session
+ end
+
+ factory :crash_dump, :class => JamRuby::CrashDump do
+ end
+end
diff --git a/ruby/spec/jam_ruby/connection_manager_spec.rb b/ruby/spec/jam_ruby/connection_manager_spec.rb
new file mode 100644
index 000000000..0056e9486
--- /dev/null
+++ b/ruby/spec/jam_ruby/connection_manager_spec.rb
@@ -0,0 +1,434 @@
+require 'spec_helper'
+
+# these tests avoid the use of ActiveRecord and FactoryGirl to do blackbox, non test-instrumented tests
+describe ConnectionManager do
+
+ TRACKS = [{"instrument_id" => "electric guitar", "sound" => "mono"}]
+
+ before do
+ @conn = PG::Connection.new(:dbname => SpecDb::TEST_DB_NAME, :user => "postgres", :password => "postgres", :host => "localhost")
+ @connman = ConnectionManager.new(:conn => @conn)
+ @message_factory = MessageFactory.new
+ end
+
+ def create_user(first_name, last_name, email, options = {:musician => true})
+ @conn.exec("INSERT INTO users (first_name, last_name, email, musician, encrypted_password, city, state, country) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id", [first_name, last_name, email, options[:musician], '1', 'Apex', 'NC', 'USA']) do |result|
+ return result.getvalue(0, 0)
+ end
+ end
+
+ def create_music_session(user_id, options={})
+ default_options = {:musician_access => true, :fan_chat => true, :fan_access => true, :approval_required=> false}
+ options = default_options.merge(options)
+ description = "some session"
+ @conn.exec("INSERT INTO music_sessions (user_id, description, musician_access, approval_required, fan_chat, fan_access) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id", [user_id, description, options[:musician_access], options[:approval_required], options[:fan_chat], options[:fan_access]]) do |result|
+ session_id = result.getvalue(0, 0)
+ @conn.exec("INSERT INTO music_sessions_history (music_session_id, description, user_id) VALUES ($1, $2, $3)", [session_id, description, user_id])
+ return session_id
+ end
+ end
+
+ def assert_num_connections(client_id, expected_num_connections)
+ # make sure the connection is still there
+ @conn.exec("SELECT count(*) FROM connections where client_id = $1", [client_id]) do |result|
+ result.getvalue(0, 0).to_i.should == expected_num_connections
+ end
+ end
+
+ def assert_session_exists(music_session_id, exists)
+ @conn.exec("SELECT count(*) FROM music_sessions where id = $1", [music_session_id]) do |result|
+ if exists
+ result.getvalue(0, 0).should == "1"
+ else
+ result.getvalue(0, 0).should == "0"
+ end
+ end
+ end
+
+ it "can't create bogus user_id" do
+ expect { @connman.create_connection("aeonuthaoentuh", "client_id", "1.1.1.1") }.to raise_error(PG::Error)
+ end
+
+ it "can't create two client_ids of same value" do
+
+ client_id = "client_id1"
+ user_id = create_user("test", "user1", "user1@jamkazam.com")
+
+ @connman.create_connection(user_id, client_id, "1.1.1.1")
+ expect { @connman.create_connection(user_id, client_id, "1.1.1.1") }.to raise_error(PG::Error)
+ end
+
+ it "create connection then delete it" do
+
+ client_id = "client_id2"
+ user_id = create_user("test", "user2", "user2@jamkazam.com")
+ count = @connman.create_connection(user_id, client_id, "1.1.1.1")
+ count.should == 1
+ # make sure the connection is seen
+
+ @conn.exec("SELECT count(*) FROM connections where user_id = $1", [user_id]) do |result|
+ result.getvalue(0, 0).should == "1"
+ end
+
+ cc = Connection.find_by_client_id!(client_id)
+ cc.connected?.should be_true
+
+ count = @connman.delete_connection(client_id)
+ count.should == 0
+
+ @conn.exec("SELECT count(*) FROM connections where user_id = $1", [user_id]) do |result|
+ result.getvalue(0, 0).should == "0"
+ end
+ end
+
+ # it "create connection creates user joined message appropriately" do
+
+ # client_id = "client_id3"
+ # client_id2 = "client_id3_1"
+
+ # user_id = create_user("test", "user3", "user3@jamkazam.com")
+
+ # # we should get a message saying that this user is online
+ # friend_update = @message_factory.friend_update(user_id, true)
+ # @connman.mq_router.should_receive(:publish_to_friends).with([], friend_update, user_id)
+
+ # @connman.create_connection(user_id, client_id, "1.1.1.1")
+
+ # # but a second connection from the same user should cause no such message
+ # @connman.should_receive(:publish_to_friends).exactly(0).times
+
+ # @connman.create_connection(user_id, client_id2, "1.1.1.1")
+
+ # end
+
+
+ # it "deletes connection creates user left message appropriately" do
+
+ # client_id = "client_id4"
+ # client_id2 = "client_id4_1"
+
+ # user_id = create_user("test", "user4", "user4@jamkazam.com")
+
+ # # we should get a message saying that this user is online
+
+ # @connman.create_connection(user_id, client_id, "1.1.1.1")
+ # @connman.create_connection(user_id, client_id2, "1.1.1.1")
+
+ # # deleting one of the two connections should cause no messages
+ # @connman.should_receive(:publish_to_friends).exactly(0).times
+
+ # @connman.delete_connection(client_id)
+
+ # # but deleting the final connection should cause a left message
+ # friend_update = @message_factory.friend_update(user_id, false)
+ # @connman.mq_router.should_receive(:publish_to_friends).with([], friend_update, user_id)
+
+ # @connman.delete_connection(client_id2)
+ # end
+
+ it "lookup of friends should find mutual friends only" do
+
+ def create_friend(user_id, friend_id)
+ @conn.exec("INSERT INTO friendships(user_id, friend_id) VALUES ($1, $2)", [user_id, friend_id])
+ end
+
+ def delete_friend(user_id, friend_id)
+ @conn.exec("DELETE FROM friendships WHERE user_id = $1 AND friend_id = $2", [user_id, friend_id])
+ end
+
+ client_id = "client_id5"
+
+ user_id1 = create_user("test", "user5", "user5@jamkazam.com")
+ user_id2 = create_user("test", "user6", "user6@jamkazam.com")
+ user_id3 = create_user("test", "user7", "user7@jamkazam.com")
+
+ @connman.gather_friends(@conn, user_id1).should == []
+ @connman.gather_friends(@conn, user_id2).should == []
+ @connman.gather_friends(@conn, user_id3).should == []
+
+ # create one-way link
+ create_friend(user_id1, user_id2)
+
+ @connman.gather_friends(@conn, user_id1).should == []
+ @connman.gather_friends(@conn, user_id2).should == []
+ @connman.gather_friends(@conn, user_id3).should == []
+
+ # create one-way link back the other way
+ create_friend(user_id2, user_id1)
+
+ @connman.gather_friends(@conn, user_id1).should == [user_id2]
+ @connman.gather_friends(@conn, user_id2).should == [user_id1]
+ @connman.gather_friends(@conn, user_id3).should == []
+
+ # make sure a new link to user 1 > user 3 doesn't disrupt anything
+ create_friend(user_id1, user_id3)
+
+ @connman.gather_friends(@conn, user_id1).should == [user_id2]
+ @connman.gather_friends(@conn, user_id2).should == [user_id1]
+ @connman.gather_friends(@conn, user_id3).should == []
+
+ # make sure a new link to user 1 > user 3 doesn't disrupt anything
+ create_friend(user_id3, user_id1)
+
+ @connman.gather_friends(@conn, user_id1).should =~ [user_id2, user_id3]
+ @connman.gather_friends(@conn, user_id2).should == [user_id1]
+ @connman.gather_friends(@conn, user_id3).should == [user_id1]
+ end
+
+ 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, "1.1.1.1")
+
+ num = JamRuby::Connection.count(:conditions => ['aasm_state = ?','connected'])
+ num.should == 1
+ assert_num_connections(client_id, num)
+ @connman.flag_stale_connections(60)
+ assert_num_connections(client_id, num)
+
+ sleep(1)
+
+ num = JamRuby::Connection.count(:conditions => ["updated_at < (NOW() - interval '#{1} second') AND aasm_state = 'connected'"])
+ num.should == 1
+ # this should change the aasm_state to stale
+ @connman.flag_stale_connections(1)
+
+ num = JamRuby::Connection.count(:conditions => ["updated_at < (NOW() - interval '#{1} second') AND aasm_state = 'connected'"])
+ num.should == 0
+
+ num = JamRuby::Connection.count(:conditions => ["updated_at < (NOW() - interval '#{1} second') AND aasm_state = 'stale'"])
+ num.should == 1
+ assert_num_connections(client_id, 1)
+
+ cids = @connman.stale_connection_client_ids(1)
+ cids.size.should == 1
+ cids[0].should == client_id
+ cids.each { |cid| @connman.delete_connection(cid) }
+
+ sleep(1)
+ assert_num_connections(client_id, 0)
+ end
+
+ 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, "1.1.1.1")
+
+ sleep(1)
+ @connman.flag_stale_connections(1)
+ assert_num_connections(client_id, 1)
+ # assert_num_connections(client_id, JamRuby::Connection.count(:conditions => ['aasm_state = ?','stale']))
+
+ @connman.expire_stale_connections(60)
+ assert_num_connections(client_id, 1)
+
+ sleep(1)
+ # this should delete the stale connection
+ @connman.expire_stale_connections(1)
+ assert_num_connections(client_id, 0)
+ end
+
+ it "connections with music_sessions associated" do
+
+ client_id = "client_id9"
+ user_id = create_user("test", "user9", "user9@jamkazam.com")
+ music_session_id = create_music_session(user_id)
+
+ user = User.find(user_id)
+ music_session = MusicSession.find(music_session_id)
+
+ @connman.create_connection(user_id, client_id, "1.1.1.1")
+ connection = @connman.join_music_session(user, client_id, music_session, true, TRACKS)
+
+ connection.errors.any?.should be_false
+
+ assert_session_exists(music_session_id, true)
+
+ @conn.exec("SELECT music_session_id FROM connections WHERE client_id = $1", [client_id]) do |result|
+ result.getvalue(0, 0).should == music_session_id
+ end
+
+ @connman.delete_connection(client_id)
+ assert_num_connections(client_id, 0)
+
+ assert_session_exists(music_session_id, false)
+ end
+
+ it "join_music_session fails if no connection" do
+
+ client_id = "client_id10"
+ user_id = create_user("test", "user10", "user10@jamkazam.com")
+ music_session_id = create_music_session(user_id)
+
+ user = User.find(user_id)
+ music_session = MusicSession.find(music_session_id)
+
+ expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS) }.to raise_error(ActiveRecord::RecordNotFound)
+
+ end
+
+ it "join_music_session fails if user is a fan but wants to join as a musician" do
+
+ client_id = "client_id10.11"
+ 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, "1.1.1.1")
+ @connman.create_connection(user_id2, client_id2, "1.1.1.1")
+
+ music_session_id = create_music_session(user_id)
+
+ user = User.find(user_id)
+ music_session = MusicSession.find(music_session_id)
+
+ @connman.join_music_session(user, client_id, music_session, true, TRACKS)
+
+ user = User.find(user_id2)
+
+ connection = @connman.join_music_session(user, client_id2, music_session, true, TRACKS)
+ connection.errors.size.should == 1
+ connection.errors.get(:as_musician).should == [Connection::FAN_CAN_NOT_JOIN_AS_MUSICIAN]
+ end
+
+ it "as_musician is coerced to boolean" do
+ client_id = "client_id10.2"
+ user_id = create_user("test", "user10.2", "user10.2@jamkazam.com", :musician => false)
+ @connman.create_connection(user_id, client_id, "1.1.1.1")
+
+ music_session_id = create_music_session(user_id)
+
+ user = User.find(user_id)
+ music_session = MusicSession.find(music_session_id)
+
+ connection = @connman.join_music_session(user, client_id, music_session, 'blarg', TRACKS)
+ connection.errors.size.should == 0
+ connection.as_musician.should be_false
+ end
+
+ it "join_music_session fails if fan_access=false and the user is a fan" do
+
+ musician_client_id = "client_id10.3"
+ 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, "1.1.1.1")
+ @connman.create_connection(fan_id, fan_client_id, "1.1.1.1")
+
+ music_session_id = create_music_session(musician_id, :fan_access => false)
+
+ user = User.find(musician_id)
+ music_session = MusicSession.find(music_session_id)
+
+ @connman.join_music_session(user, musician_client_id, music_session, true, TRACKS)
+
+ # now join the session as a fan, bt fan_access = false
+ user = User.find(fan_id)
+ connection = @connman.join_music_session(user, fan_client_id, music_session, false, TRACKS)
+ connection.errors.size.should == 1
+ end
+
+ it "join_music_session fails if incorrect user_id specified" do
+
+ client_id = "client_id20"
+ user_id = create_user("test", "user20", "user20@jamkazam.com")
+ user_id2 = create_user("test", "user21", "user21@jamkazam.com")
+ music_session_id = create_music_session(user_id)
+
+ user = User.find(user_id2)
+ music_session = MusicSession.find(music_session_id)
+
+ @connman.create_connection(user_id, client_id, "1.1.1.1")
+ # specify real user id, but not associated with this session
+ expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS) } .to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it "join_music_session fails if no music_session" do
+ client_id = "client_id11"
+ user_id = create_user("test", "user11", "user11@jamkazam.com")
+
+ user = User.find(user_id)
+ music_session = MusicSession.new
+
+ @connman.create_connection(user_id, client_id, "1.1.1.1")
+ connection = @connman.join_music_session(user, client_id, music_session, true, TRACKS)
+ connection.errors.size.should == 1
+ connection.errors.get(:music_session).should == [Connection::MUSIC_SESSION_MUST_BE_SPECIFIED]
+ end
+
+ it "join_music_session fails if approval_required and no invitation, but generates join_request" do
+ client_id = "client_id11.1"
+ user_id = create_user("test", "user11.1", "user11.1@jamkazam.com")
+ user_id2 = create_user("test", "user11.2", "user11.2@jamkazam.com")
+ music_session_id = create_music_session(user_id, :approval_required => true)
+
+ user = User.find(user_id2)
+ music_session = MusicSession.find(music_session_id)
+
+ @connman.create_connection(user_id, client_id, "1.1.1.1")
+ # specify real user id, but not associated with this session
+ expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS) } .to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+
+ it "leave_music_session fails if no music_session" do
+
+ client_id = "client_id12"
+ user_id = create_user("test", "user12", "user12@jamkazam.com")
+
+ user = User.find(user_id)
+ dummy_music_session = MusicSession.new
+
+ @connman.create_connection(user_id, client_id, "1.1.1.1")
+
+ expect { @connman.leave_music_session(user, Connection.find_by_client_id(client_id), dummy_music_session) }.to raise_error(JamRuby::StateError)
+ end
+
+ it "leave_music_session fails if in different music_session" do
+
+ client_id = "client_id13"
+ user_id = create_user("test", "user13", "user13@jamkazam.com")
+ music_session_id = create_music_session(user_id)
+
+ user = User.find(user_id)
+ music_session = MusicSession.find(music_session_id)
+
+ dummy_music_session = MusicSession.new
+
+ @connman.create_connection(user_id, client_id, "1.1.1.1")
+ @connman.join_music_session(user, client_id, music_session, true, TRACKS)
+ expect { @connman.leave_music_session(user, Connection.find_by_client_id(client_id), dummy_music_session) }.to raise_error(JamRuby::StateError)
+ end
+
+ it "leave_music_session works" do
+
+ client_id = "client_id14"
+ user_id = create_user("test", "user14", "user14@jamkazam.com")
+ music_session_id = create_music_session(user_id)
+
+ user = User.find(user_id)
+ music_session = MusicSession.find(music_session_id)
+
+ @connman.create_connection(user_id, client_id, "1.1.1.1")
+ @connman.join_music_session(user, client_id, music_session, true, TRACKS)
+
+ assert_session_exists(music_session_id, true)
+
+ @conn.exec("SELECT music_session_id FROM connections WHERE client_id = $1", [client_id]) do |result|
+ result.getvalue(0, 0).should == music_session_id
+ end
+
+ @connman.leave_music_session(user, Connection.find_by_client_id(client_id), music_session)
+
+ @conn.exec("SELECT music_session_id FROM connections WHERE client_id = $1", [client_id]) do |result|
+ result.getvalue(0, 0).should == nil
+ end
+
+ assert_session_exists(music_session_id, false)
+
+ @connman.delete_connection(client_id)
+
+ assert_num_connections(client_id, 0)
+
+ end
+end
+
diff --git a/ruby/spec/jam_ruby/lib/profanity_spec.rb b/ruby/spec/jam_ruby/lib/profanity_spec.rb
new file mode 100644
index 000000000..4da77b657
--- /dev/null
+++ b/ruby/spec/jam_ruby/lib/profanity_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Profanity do
+
+ describe "profanity_filter" do
+
+ it "can handle a nil input" do
+ Profanity.is_profane?(nil).should be_false
+ end
+
+ it "can handle a blank input" do
+ Profanity.is_profane?('').should be_false
+ end
+
+ it "can handle a clean input" do
+ Profanity.is_profane?('you are a clean input').should be_false
+ end
+
+ it "can handle a profane input" do
+ Profanity.is_profane?('fuck you!').should be_true
+ end
+
+ it "is not fooled by punctuation" do
+ Profanity.is_profane?('fuck-you!').should be_true
+ Profanity.is_profane?('???$$fuck-you!').should be_true
+ Profanity.is_profane?('--!fuck-you!').should be_true
+ end
+ end
+
+end
+
diff --git a/ruby/spec/jam_ruby/lib/s3_util_spec.rb b/ruby/spec/jam_ruby/lib/s3_util_spec.rb
new file mode 100644
index 000000000..7fb01e844
--- /dev/null
+++ b/ruby/spec/jam_ruby/lib/s3_util_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe S3Util do
+
+ describe "sign_url" do
+ it "returns something" do
+ S3Util.sign_url("jamkazam-dev", "avatar-tmp/user/image.png").should_not be_nil
+ end
+ end
+
+end
+
diff --git a/ruby/spec/jam_ruby/models/artifact_update_spec.rb b/ruby/spec/jam_ruby/models/artifact_update_spec.rb
new file mode 100644
index 000000000..24075e310
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/artifact_update_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+require 'digest/md5'
+
+describe ArtifactUpdate do
+
+ include UsesTempFiles
+
+ ARTIFACT_FILE='jkclient-0.1.1.exe'
+
+ in_directory_with_file(ARTIFACT_FILE)
+
+ before do
+ content_for_file("exe binary globby goo")
+ end
+
+ it "return empty" do
+ ArtifactUpdate.find(:all).length.should == 0
+ end
+
+
+ it "should allow insertion" do
+
+ artifact = ArtifactUpdate.new
+ artifact.product = 'JamClient/Win32'
+ artifact.version = '0.1.1'
+ artifact.uri = File.open(ARTIFACT_FILE)
+ artifact.save!
+
+ artifact.environment.should == "public"
+ artifact.product.should == "JamClient/Win32"
+ artifact.version.should == "0.1.1"
+ File.basename(artifact.uri.path).should == ARTIFACT_FILE
+ artifact.sha1.should == Digest::MD5.hexdigest(File.read(ARTIFACT_FILE))
+ artifact.size.should == File.size(ARTIFACT_FILE)
+
+ found = ArtifactUpdate.find_by_product_and_version('JamClient/Win32', '0.1.1')
+ artifact.should == found
+ end
+
+end
\ No newline at end of file
diff --git a/ruby/spec/jam_ruby/models/band_search_spec.rb b/ruby/spec/jam_ruby/models/band_search_spec.rb
new file mode 100644
index 000000000..99af288f8
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/band_search_spec.rb
@@ -0,0 +1,114 @@
+require 'spec_helper'
+
+describe User do
+
+ let(:user) { FactoryGirl.create(:user) }
+
+ before(:each) do
+
+ @band = Band.save(nil, "Example Band", "www.bands.com", "zomg we rock", "Apex", "NC", "USA", ["hip hop"], user.id, nil, nil)
+
+ end
+
+ it "should allow search of one band with an exact match" do
+ ws = Band.search("Example Band")
+ ws.length.should == 1
+ band_result = ws[0]
+ band_result.name.should == @band.name
+ band_result.id.should == @band.id
+ band_result.location.should == @band.location
+ end
+
+ it "should allow search of one band with partial matches" do
+ ws = Band.search("Ex")
+ ws.length.should == 1
+ ws[0].id.should == @band.id
+
+ ws = Band.search("Exa")
+ ws.length.should == 1
+ ws[0].id.should == @band.id
+
+ ws = Band.search("Exam")
+ ws.length.should == 1
+ ws[0].id.should == @band.id
+
+ ws = Band.search("Examp")
+ ws.length.should == 1
+ ws[0].id.should == @band.id
+
+ ws = Band.search("Exampl")
+ ws.length.should == 1
+ ws[0].id.should == @band.id
+
+ ws = Band.search("Example")
+ ws.length.should == 1
+ ws[0].id.should == @band.id
+
+ ws = Band.search("Ba")
+ ws.length.should == 1
+ ws[0].id.should == @band.id
+
+ ws = Band.search("Ban")
+ ws.length.should == 1
+ ws[0].id.should == @band.id
+ end
+
+ it "should not match mid-word searchs" do
+ ws = Band.search("xa")
+ ws.length.should == 0
+
+ ws = Band.search("le")
+ ws.length.should == 0
+ end
+
+ it "should delete band" do
+ ws = Band.search("Example Band")
+ ws.length.should == 1
+ band_result = ws[0]
+ band_result.id.should == @band.id
+
+ @band.destroy # delete doesn't work; you have to use destroy.
+
+ ws = Band.search("Example Band")
+ ws.length.should == 0
+ end
+
+ it "should update band" do
+ ws = Band.search("Example Band")
+ ws.length.should == 1
+ band_result = ws[0]
+ band_result.id.should == @band.id
+
+ @band.name = "bonus-stuff"
+ @band.save
+
+ ws = Band.search("Example Band")
+ ws.length.should == 0
+
+ ws = Band.search("Bonus")
+ ws.length.should == 1
+ band_result = ws[0]
+ band_result.id.should == @band.id
+ band_result.name.should == "bonus-stuff"
+ end
+
+ it "should tokenize correctly" do
+ @band2 = Band.save(nil, "Peach pit", "www.bands.com", "zomg we rock", "Apex", "NC", "USA", ["hip hop"], user.id, nil, nil)
+ ws = Band.search("pea")
+ ws.length.should == 1
+ user_result = ws[0]
+ user_result.id.should == @band2.id
+ end
+
+
+ it "should not return anything with a 1 character search" do
+ @band2 = Band.save(nil, "Peach pit", "www.bands.com", "zomg we rock", "Apex", "NC", "USA", ["hip hop"], user.id, nil, nil)
+ ws = Band.search("pe")
+ ws.length.should == 1
+ user_result = ws[0]
+ user_result.id.should == @band2.id
+
+ ws = Band.search("p")
+ ws.length.should == 0
+ end
+end
\ No newline at end of file
diff --git a/ruby/spec/jam_ruby/models/connection_spec.rb b/ruby/spec/jam_ruby/models/connection_spec.rb
new file mode 100644
index 000000000..29d23f476
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/connection_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Connection do
+ let(:user) { FactoryGirl.create(:user) }
+ let (:music_session) { FactoryGirl.create(:music_session, :creator => user) }
+
+ it 'starts in the correct state' do
+ connection = FactoryGirl.create(:connection,
+ :user => user,
+ :music_session => music_session,
+ :ip_address => "1.1.1.1",
+ :client_id => "1")
+
+ connection.idle?.should be_true
+ end
+
+ it 'transitions properly' do
+ connection = FactoryGirl.create(:connection,
+ :user => user,
+ :music_session => music_session,
+ :ip_address => "1.1.1.1",
+ :client_id => "1")
+
+ connection.connect!
+ connection.connected?.should be_true
+ connection.state_message.should == 'Connected'
+
+ connection.stale!
+ connection.stale?.should be_true
+ connection.state_message.should == 'Stale'
+
+ connection.expire!
+ connection.destroyed?.should be_true
+ end
+
+end
diff --git a/ruby/spec/jam_ruby/models/crash_dump_spec.rb b/ruby/spec/jam_ruby/models/crash_dump_spec.rb
new file mode 100644
index 000000000..c395f1038
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/crash_dump_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe CrashDump do
+ before do
+ end
+
+ it "should fail to save a crash dump without a client_type and client_version" do
+ CrashDump.new(:client_type => "", :client_version => "version").should_not be_valid
+ CrashDump.new(:client_type => "type", :client_version => "").should_not be_valid
+ end
+
+ it "should be able to save a crash dump with JUST a client_type and client_version" do
+ @cd = CrashDump.new
+ @cd.client_type = "Win32"
+ @cd.client_version = "version"
+ @cd.should be_valid
+ @cd.save
+
+ CrashDump.first.id.should == @cd.id
+ end
+
+end
diff --git a/ruby/spec/jam_ruby/models/feedback_spec.rb b/ruby/spec/jam_ruby/models/feedback_spec.rb
new file mode 100644
index 000000000..bf1260c76
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/feedback_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Feedback do
+
+ let(:feedback) { Feedback.new }
+
+ before(:each) do
+ CorpMailer.deliveries.clear
+ end
+
+ describe "empty model" do
+
+ before(:each) do
+ feedback.save
+ end
+
+ it { feedback.valid?.should be_false }
+ it { feedback.errors.keys.length.should == 2}
+ it { feedback.errors["email"].length.should == 2}
+ it { feedback.errors["email"][0].include?("blank").should be_true}
+ it { feedback.errors["email"][1].include?("invalid").should be_true}
+ it { feedback.errors["body"].length.should == 1}
+ it { feedback.errors["body"][0].include?("blank").should be_true}
+ it { CorpMailer.deliveries.length.should == 0}
+
+ end
+
+ describe "bad email" do
+ before(:each) do
+ feedback.email = "blarg"
+ feedback.body = "here's the problem!"
+ feedback.save
+ end
+
+ it { feedback.valid?.should be_false }
+ it { feedback.errors.keys.length.should == 1}
+ it { feedback.errors["email"].length.should == 1}
+ it { feedback.errors["email"][0].include?("invalid").should be_true}
+ it { CorpMailer.deliveries.length.should == 0}
+ end
+
+ describe "populated model" do
+ before(:each) do
+ feedback.email = "seth@jamkazam.com"
+ feedback.body = "here's the problem!"
+ feedback.save
+ end
+
+ it { feedback.valid?.should be_true }
+ it { feedback.errors.keys.length.should == 0 }
+ it { CorpMailer.deliveries.length.should == 1}
+ end
+end
+
diff --git a/ruby/spec/jam_ruby/models/invitation_spec.rb b/ruby/spec/jam_ruby/models/invitation_spec.rb
new file mode 100644
index 000000000..b5e1cb07f
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/invitation_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe MusicSession do
+
+ it 'cant create invitation to non-friend' do
+
+ user1 = FactoryGirl.create(:user) # in the jam session
+ user2 = FactoryGirl.create(:user) # in the jam session
+
+ music_session = FactoryGirl.create(:music_session, :creator => user1)
+
+ music_session_member1 = FactoryGirl.create(:connection, :user => user1, :music_session => music_session, :ip_address => "1.1.1.1", :client_id => "1")
+ music_session_member2 = FactoryGirl.create(:connection, :user => user2, :music_session => music_session, :ip_address => "2.2.2.2", :client_id => "2")
+
+ invitation = Invitation.new(:sender => user1, :receiver => user2, :music_session => music_session)
+
+ invitation.save.should be_false
+ invitation.errors.size.should == 1
+ invitation.errors.get(:receiver).should == [Invitation::FRIENDSHIP_REQUIRED_VALIDATION_ERROR]
+ end
+
+ it 'can create invitation to friend' do
+
+ user1 = FactoryGirl.create(:user) # in the jam session
+ user2 = FactoryGirl.create(:user) # in the jam session
+
+ music_session = FactoryGirl.create(:music_session, :creator => user1)
+
+ music_session_member1 = FactoryGirl.create(:connection, :user => user1, :music_session => music_session, :ip_address => "1.1.1.1", :client_id => "1")
+ music_session_member2 = FactoryGirl.create(:connection, :user => user2, :music_session => music_session, :ip_address => "2.2.2.2", :client_id => "2")
+
+ FactoryGirl.create(:friendship, :user => user1, :friend => user2)
+ FactoryGirl.create(:friendship, :user => user2, :friend => user1)
+
+ invitation = Invitation.new(:sender => user1, :receiver => user2, :music_session => music_session)
+
+ invitation.save.should be_true
+ end
+
+ it 'can create invitation to a user who made a join_request' do
+ user1 = FactoryGirl.create(:user) # in the jam session
+ user2 = FactoryGirl.create(:user) # in the jam session
+
+ music_session = FactoryGirl.create(:music_session, :creator => user1)
+
+ music_session_member1 = FactoryGirl.create(:connection, :user => user1, :music_session => music_session, :ip_address => "1.1.1.1", :client_id => "1")
+ connection2 = FactoryGirl.create(:connection, :user => user2, :ip_address => "2.2.2.2", :client_id => "2")
+
+ join_request = FactoryGirl.create(:join_request, :user => user2, :music_session => music_session)
+
+ invitation = Invitation.new(:sender => user1, :receiver => user2, :music_session => music_session, :join_request => join_request)
+
+ invitation.save.should be_true
+ end
+
+ it 'cant create invitation to a user who did not make a join_request and is not a friend' do
+ user1 = FactoryGirl.create(:user) # in the jam session
+ user2 = FactoryGirl.create(:user) # in the jam session
+
+ music_session = FactoryGirl.create(:music_session, :creator => user1)
+ music_session2 = FactoryGirl.create(:music_session, :creator => user1)
+
+ music_session_member1 = FactoryGirl.create(:connection, :user => user1, :music_session => music_session, :ip_address => "1.1.1.1", :client_id => "1")
+ connection2 = FactoryGirl.create(:connection, :user => user2, :ip_address => "2.2.2.2", :client_id => "2")
+
+ join_request = FactoryGirl.create(:join_request, :user => user2, :music_session => music_session2)
+
+ invitation = Invitation.new(:sender => user1, :receiver => user2, :music_session => music_session, :join_request => join_request)
+
+ invitation.save.should be_false
+ invitation.errors.get(:join_request).should == [Invitation::JOIN_REQUEST_IS_NOT_FOR_RECEIVER_AND_MUSIC_SESSION ]
+ end
+end
diff --git a/ruby/spec/jam_ruby/models/invited_user_spec.rb b/ruby/spec/jam_ruby/models/invited_user_spec.rb
new file mode 100644
index 000000000..b3de880b5
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/invited_user_spec.rb
@@ -0,0 +1,105 @@
+require 'spec_helper'
+
+describe InvitedUser do
+
+ before(:each) do
+ UserMailer.deliveries.clear
+ end
+
+ it 'create an invitation from end-user' do
+
+ # create an end user
+ user1 = FactoryGirl.create(:user)
+
+ # create the invitation from the end-user
+ invited_user = FactoryGirl.create(:invited_user, :sender => user1)
+
+ invited_user.email.should_not be_nil
+ invited_user.sender.should_not be_nil
+ invited_user.note.should be_nil
+ invited_user.invited_by_administrator?.should be_false
+ end
+
+ it 'create an invitation from admin-user' do
+
+ # create an admin user
+ user1 = FactoryGirl.create(:admin)
+
+ # create the invitation from the end-user
+ invited_user = FactoryGirl.create(:invited_user, :sender => user1)
+
+ invited_user.email.should_not be_nil
+ invited_user.sender.should_not be_nil
+ invited_user.note.should be_nil
+ invited_user.invited_by_administrator?.should be_true
+ end
+
+ it 'create an invitation from no one in particular' do
+ # create the invitation from the end-user
+ invited_user = FactoryGirl.build(:invited_user)
+
+ invited_user.invited_by_administrator?.should be_true
+ end
+
+ it 'email is sent automatically by virtue of observer' do
+ # create an admin user
+ user1 = FactoryGirl.create(:admin)
+
+ # create the invitation from the end-user
+ invited_user = FactoryGirl.create(:invited_user, :sender => user1)
+
+ InvitedUserMailer.deliveries.length.should == 1
+ end
+
+ it 'accept an invitation' do
+ # create an admin user
+ user1 = FactoryGirl.create(:admin)
+
+ # create the invitation from the end-user
+ invited_user = FactoryGirl.create(:invited_user, :sender => user1)
+
+ invited_user.accepted.should be_false
+
+ invited_user.accept!
+ invited_user.save
+
+ invited_user.accepted.should be_true
+ end
+
+ it 'checks can invite' do
+ # create an admin user
+ user1 = FactoryGirl.create(:user)
+ user1.can_invite = false
+ user1.save
+
+ # create the invitation from the end-user
+ invited_user = FactoryGirl.build(:invited_user, :sender => user1)
+ invited_user.save
+ invited_user.errors.any?.should be_true
+ end
+
+ it 'list invites for a user' do
+ # user to create an invite with
+ user1 = FactoryGirl.create(:user)
+
+ InvitedUser.index(user1).length.should == 0
+
+ # create the invitation from the end-user
+ invited_user = FactoryGirl.create(:invited_user, :sender => user1)
+ invited_users = InvitedUser.index(user1)
+ invited_users.length.should == 1
+ invited_users[0].should == invited_user
+ end
+
+ it 'should not allow note to have profanity' do
+
+ user1 = FactoryGirl.create(:user)
+
+ # create the invitation from the end-user
+ invited_user = FactoryGirl.create(:invited_user, :sender => user1)
+ invited_user.note = 'fuck you'
+ invited_user.save
+ invited_user.valid?.should be_false
+ end
+
+end
diff --git a/ruby/spec/jam_ruby/models/join_request_spec.rb b/ruby/spec/jam_ruby/models/join_request_spec.rb
new file mode 100644
index 000000000..14412556f
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/join_request_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe JoinRequest do
+
+ it 'can create a join request' do
+ user1 = FactoryGirl.create(:user)
+ music_session = FactoryGirl.create(:music_session, :creator => user1)
+ music_session_member1 = FactoryGirl.create(:connection, :user => user1, :music_session => music_session)
+ join_request = JoinRequest.new(:user => user1, :music_session => music_session, :text => "Let me join yo")
+
+ join_request.save.should be_true
+
+ join_requests = JoinRequest.index(user1)
+ join_requests.length.should == 1
+ join_requests[0].id.should == join_request.id
+ end
+
+ it 'fans cant create a join request' do
+ user1 = FactoryGirl.create(:user, :musician => true)
+ user2 = FactoryGirl.create(:user, :musician => false)
+ music_session = FactoryGirl.create(:music_session, :creator => user1)
+ music_session_member1 = FactoryGirl.create(:connection, :user => user1, :music_session => music_session)
+ join_request = JoinRequest.new(:user => user2, :music_session => music_session, :text => "Let me join yo")
+
+ join_request.save.should be_false
+ join_request.errors.size.should == 1
+ join_request.errors.get(:user).should == [JoinRequest::REQUESTOR_MUST_BE_A_MUSICIAN]
+ end
+
+ it 'cant create a dup join_request' do
+ user1 = FactoryGirl.create(:user)
+ music_session = FactoryGirl.create(:music_session, :creator => user1)
+ music_session_member1 = FactoryGirl.create(:connection, :user => user1, :music_session => music_session)
+ join_request = JoinRequest.new(:user => user1, :music_session => music_session, :text => "Let me join yo")
+ join_request.save.should be_true
+
+ join_request2 = JoinRequest.new(:user => user1, :music_session => music_session, :text => "Let me join yo")
+
+ join_request2.save.should be_false
+ join_request2.errors.get(:user_id) == ["has already been taken"]
+ end
+
+ it "cant contain profanity in the text" do
+ user1 = FactoryGirl.create(:user)
+ music_session = FactoryGirl.create(:music_session, :creator => user1)
+ music_session_member1 = FactoryGirl.create(:connection, :user => user1, :music_session => music_session)
+ join_request = JoinRequest.new(:user => user1, :music_session => music_session, :text => "fuck you")
+ join_request.save
+ join_request.valid?.should be_false
+ end
+end
diff --git a/ruby/spec/jam_ruby/models/max_mind_geo_spec.rb b/ruby/spec/jam_ruby/models/max_mind_geo_spec.rb
new file mode 100644
index 000000000..ba49462de
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/max_mind_geo_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe MaxMindGeo do
+
+ include UsesTempFiles
+
+ GEO_CSV='small.csv'
+
+ in_directory_with_file(GEO_CSV)
+
+ before do
+
+ content_for_file('startIpNum,endIpNum,country,region,city,postalCode,latitude,longitude,dmaCode,areaCode
+0.116.0.0,0.119.255.255,"AT","","","",47.3333,13.3333,,
+1.0.0.0,1.0.0.255,"AU","","","",-27.0000,133.0000,,
+1.0.1.0,1.0.1.255,"CN","07","Fuzhou","",26.0614,119.3061,,'.encode(Encoding::ISO_8859_1))
+
+ MaxMindGeo.import_from_max_mind(GEO_CSV)
+ end
+
+ let(:first) { MaxMindGeo.find_by_ip_bottom(MaxMindGeo.ip_address_to_int('0.116.0.0')) }
+ let(:second) { MaxMindGeo.find_by_ip_bottom(MaxMindGeo.ip_address_to_int('1.0.0.0')) }
+ let(:third) { MaxMindGeo.find_by_ip_bottom(MaxMindGeo.ip_address_to_int('1.0.1.0')) }
+
+ it { MaxMindGeo.count.should == 3 }
+
+ it { first.country.should == 'AT' }
+ it { first.ip_bottom.should == MaxMindGeo.ip_address_to_int('0.116.0.0') }
+ it { first.ip_top.should == MaxMindGeo.ip_address_to_int('0.119.255.255') }
+
+ it { second.country.should == 'AU' }
+ it { second.ip_bottom.should == MaxMindGeo.ip_address_to_int('1.0.0.0') }
+ it { second.ip_top.should == MaxMindGeo.ip_address_to_int('1.0.0.255') }
+
+ it { third.country.should == 'CN' }
+ it { third.ip_bottom.should == MaxMindGeo.ip_address_to_int('1.0.1.0') }
+ it { third.ip_top.should == MaxMindGeo.ip_address_to_int('1.0.1.255') }
+end
+
diff --git a/ruby/spec/jam_ruby/models/max_mind_isp_spec.rb b/ruby/spec/jam_ruby/models/max_mind_isp_spec.rb
new file mode 100644
index 000000000..b61f86cfc
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/max_mind_isp_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe MaxMindIsp do
+
+ include UsesTempFiles
+
+ ISP_CSV='small.csv'
+
+ in_directory_with_file(ISP_CSV)
+
+ before do
+
+ content_for_file('Copyright (c) 2011 MaxMind Inc. All Rights Reserved.
+"beginIp","endIp","countryCode","ISP"
+"1.0.0.0","1.0.0.255","AU","APNIC Debogon Project"
+"1.0.1.0","1.0.1.255","CN","Chinanet Fujian Province Network"
+"1.0.4.0","1.0.7.255","AU","Bigred,inc"'.encode(Encoding::ISO_8859_1))
+
+ MaxMindIsp.import_from_max_mind(ISP_CSV)
+ end
+
+ let(:first) { MaxMindIsp.find_by_ip_bottom(MaxMindIsp.ip_address_to_int('1.0.0.0')) }
+ let(:second) { MaxMindIsp.find_by_ip_bottom(MaxMindIsp.ip_address_to_int('1.0.1.0')) }
+ let(:third) { MaxMindIsp.find_by_ip_bottom(MaxMindIsp.ip_address_to_int('1.0.4.0')) }
+
+ it { MaxMindIsp.count.should == 3 }
+
+ it { first.country.should == 'AU' }
+ it { first.ip_bottom.should == MaxMindIsp.ip_address_to_int('1.0.0.0') }
+ it { first.ip_top.should == MaxMindIsp.ip_address_to_int('1.0.0.255') }
+ it { first.isp.should == 'APNIC Debogon Project' }
+
+ it { second.country.should == 'CN' }
+ it { second.ip_bottom.should == MaxMindIsp.ip_address_to_int('1.0.1.0') }
+ it { second.ip_top.should == MaxMindIsp.ip_address_to_int('1.0.1.255') }
+ it { second.isp.should == 'Chinanet Fujian Province Network' }
+
+ it { third.country.should == 'AU' }
+ it { third.ip_bottom.should == MaxMindIsp.ip_address_to_int('1.0.4.0') }
+ it { third.ip_top.should == MaxMindIsp.ip_address_to_int('1.0.7.255') }
+ it { third.isp.should == 'Bigred,inc' }
+end
+
diff --git a/ruby/spec/jam_ruby/models/mix_spec.rb b/ruby/spec/jam_ruby/models/mix_spec.rb
new file mode 100755
index 000000000..91f3d0463
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/mix_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Mix do
+ before do
+ @user = FactoryGirl.create(:user)
+ @connection = FactoryGirl.create(:connection, :user => @user)
+ @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
+ @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
+ @music_session = FactoryGirl.create(:music_session, :creator => @user, :musician_access => true)
+ @music_session.connections << @connection
+ @music_session.save
+ @recording = Recording.start(@music_session.id, @user)
+ @recording.stop
+ @mix = Mix.schedule(@recording, "{}")
+ end
+
+ it "should create a mix for a user's recording properly" do
+ @mix.recording_id.should == @recording.id
+ @mix.manifest.should == "{}"
+ @mix.mix_server.should be_nil
+ @mix.started_at.should be_nil
+ @mix.completed_at.should be_nil
+ end
+
+ it "should fail to create a mix if the userid doesn't own the recording" do
+ @user2 = FactoryGirl.create(:user)
+ expect { Mix.schedule(@recording) }.to raise_error
+ end
+
+ it "should fail if the recording doesn't exist" do
+ expect { @mix2 = Mix.schedule(Recording.find('lskdjflsd')) }.to raise_error
+ end
+
+ it "should return a mix when the cron asks for it" do
+ this_mix = Mix.next("server")
+ this_mix.id.should == @mix.id
+ @mix.reload
+ @mix.started_at.should_not be_nil
+ @mix.mix_server.should == "server"
+ @mix.completed_at.should be_nil
+ end
+
+ it "should record when a mix has finished" do
+ Mix.find(@mix.id).finish(10000, "md5hash")
+ @mix.reload
+ @mix.completed_at.should_not be_nil
+ @mix.length.should == 10000
+ @mix.md5.should == "md5hash"
+ end
+
+ it "should re-run a mix if it was started a long time ago" do
+ this_mix = Mix.next("server")
+ @mix.reload
+ @mix.started_at -= 1000000
+ @mix.save
+ this_mix = Mix.next("server")
+ this_mix.id.should == @mix.id
+ end
+
+end
+
+
diff --git a/ruby/spec/jam_ruby/models/music_session_history_spec.rb b/ruby/spec/jam_ruby/models/music_session_history_spec.rb
new file mode 100644
index 000000000..260917687
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/music_session_history_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe MusicSessionHistory do
+
+ let(:some_user) { FactoryGirl.create(:user) }
+ let(:music_session) { FactoryGirl.create(:music_session) }
+ let(:history) { FactoryGirl.create(:music_session_history, :music_session => music_session) }
+ let(:user_history1) { FactoryGirl.create(:music_session_user_history, :history => history, :user => music_session.creator) }
+ let(:user_history2) { FactoryGirl.create(:music_session_user_history, :history => history, :user => some_user) }
+
+ it "create" do
+ history.description.should eql(music_session.description)
+ end
+
+ it "unique users" do
+ user_history1.should_not be_nil
+ user_history2.should_not be_nil
+ users = history.unique_users
+
+ users.length.should eql(2)
+
+ users.include?(some_user).should be_true
+ users.include?(music_session.creator).should be_true
+ end
+
+end
+
+
diff --git a/ruby/spec/jam_ruby/models/music_session_perf_data_spec.rb b/ruby/spec/jam_ruby/models/music_session_perf_data_spec.rb
new file mode 100644
index 000000000..2ec72ba71
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/music_session_perf_data_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe MusicSessionPerfData do
+
+ before do
+ #music_session = FactoryGirl.create(:music_session)
+ #connection = FactoryGirl.create(:connection, :music_session => music_session)
+ end
+
+ it "create" do
+
+ end
+
+
+end
\ No newline at end of file
diff --git a/ruby/spec/jam_ruby/models/music_session_spec.rb b/ruby/spec/jam_ruby/models/music_session_spec.rb
new file mode 100644
index 000000000..6a06555d2
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/music_session_spec.rb
@@ -0,0 +1,372 @@
+require 'spec_helper'
+
+describe MusicSession do
+
+ before(:each) do
+ MusicSession.delete_all
+ end
+ it 'can grant access to valid user' do
+
+ user1 = FactoryGirl.create(:user) # in the jam session
+ user2 = FactoryGirl.create(:user) # in the jam session
+ user3 = FactoryGirl.create(:user) # not in the jam session
+
+ music_session = FactoryGirl.create(:music_session, :creator => user1, :musician_access => false)
+ FactoryGirl.create(:connection, :user => user1, :music_session => music_session)
+ FactoryGirl.create(:connection, :user => user2, :music_session => music_session)
+
+
+ music_session.access?(user1).should == true
+ music_session.access?(user2).should == true
+ music_session.access?(user3).should == false
+ end
+
+ it 'anyone can join a open music session' do
+
+ user1 = FactoryGirl.create(:user) # in the jam session
+ user2 = FactoryGirl.create(:user) # in the jam session
+ user3 = FactoryGirl.create(:user) # not in the jam session
+
+ music_session = FactoryGirl.create(:music_session, :creator => user1, :musician_access => true)
+
+ music_session.can_join?(user1, true).should == true
+ music_session.can_join?(user2, true).should == true
+ music_session.can_join?(user3, true).should == true
+ end
+
+ it 'no one but invited people can join closed music session' do
+ user1 = FactoryGirl.create(:user) # in the jam session
+ user2 = FactoryGirl.create(:user) # in the jam session
+ user3 = FactoryGirl.create(:user) # not in the jam session
+
+ music_session = FactoryGirl.create(:music_session, :creator => user1, :musician_access => false)
+ FactoryGirl.create(:connection, :user => user1, :music_session => music_session)
+
+ music_session.can_join?(user1, true).should == true
+ music_session.can_join?(user2, true).should == false
+ music_session.can_join?(user3, true).should == false
+
+ # invite user 2
+ FactoryGirl.create(:friendship, :user => user1, :friend => user2)
+ FactoryGirl.create(:friendship, :user => user2, :friend => user1)
+ FactoryGirl.create(:invitation, :sender => user1, :receiver => user2, :music_session => music_session)
+
+ music_session.can_join?(user1, true).should == true
+ music_session.can_join?(user2, true).should == true
+ music_session.can_join?(user3, true).should == false
+ end
+
+ it 'no one but invited people can see closed music session' do
+ user1 = FactoryGirl.create(:user) # in the jam session
+ user2 = FactoryGirl.create(:user) # in the jam session
+ user3 = FactoryGirl.create(:user) # not in the jam session
+
+ music_session = FactoryGirl.create(:music_session, :creator => user1, :musician_access => false)
+ FactoryGirl.create(:connection, :user => user1, :music_session => music_session)
+
+ music_session.can_see?(user1).should == true
+ music_session.can_see?(user2).should == false
+ music_session.can_see?(user3).should == false
+
+ # invite user 2
+ FactoryGirl.create(:friendship, :user => user1, :friend => user2)
+ FactoryGirl.create(:friendship, :user => user2, :friend => user1)
+ FactoryGirl.create(:invitation, :sender => user1, :receiver => user2, :music_session => music_session)
+
+ music_session.can_see?(user1).should == true
+ music_session.can_see?(user2).should == true
+ music_session.can_see?(user3).should == false
+ end
+
+
+ it "orders two sessions by created_at starting with most recent" do
+ creator = FactoryGirl.create(:user)
+
+ earlier_session = FactoryGirl.create(:music_session, :creator => creator, :description => "Earlier Session")
+ later_session = FactoryGirl.create(:music_session, :creator => creator, :description => "Later Session")
+
+ user = FactoryGirl.create(:user)
+
+ #ActiveRecord::Base.logger = Logger.new(STDOUT)
+ music_sessions = MusicSession.index(user)
+ music_sessions.length.should == 2
+ music_sessions.first.id.should == later_session.id
+ end
+
+ it "orders sessions with inviteds first, even if created first" do
+ creator = FactoryGirl.create(:user)
+ earlier_session = FactoryGirl.create(:music_session, :creator => creator, :description => "Earlier Session")
+ later_session = FactoryGirl.create(:music_session, :creator => creator, :description => "Later Session")
+ user = FactoryGirl.create(:user)
+ FactoryGirl.create(:connection, :user => creator, :music_session => earlier_session)
+ FactoryGirl.create(:friendship, :user => creator, :friend => user)
+ FactoryGirl.create(:friendship, :user => user, :friend => creator)
+ FactoryGirl.create(:invitation, :sender => creator, :receiver => user, :music_session => earlier_session)
+
+ music_sessions = MusicSession.index(user)
+ music_sessions.length.should == 2
+ music_sessions.first.id.should == earlier_session.id
+ end
+
+
+ it "orders sessions with friends in the session first, even if created first" do
+
+ creator1 = FactoryGirl.create(:user)
+ creator2 = FactoryGirl.create(:user)
+ earlier_session = FactoryGirl.create(:music_session, :creator => creator1, :description => "Earlier Session")
+ later_session = FactoryGirl.create(:music_session, :creator => creator2, :description => "Later Session")
+ user = FactoryGirl.create(:user)
+ FactoryGirl.create(:friendship, :user => creator1, :friend => user)
+ FactoryGirl.create(:friendship, :user => user, :friend => creator1)
+ FactoryGirl.create(:connection, :user => creator1, :music_session => earlier_session)
+ FactoryGirl.create(:connection, :user => creator2, :music_session => earlier_session)
+
+ music_sessions = MusicSession.index(user)
+ music_sessions.length.should == 2
+ music_sessions.first.id.should == earlier_session.id
+ end
+
+ it "doesn't list a session if musician_access is set to false" do
+ creator = FactoryGirl.create(:user)
+ session = FactoryGirl.create(:music_session, :creator => creator, :description => "Session", :musician_access => false)
+ user = FactoryGirl.create(:user)
+
+ music_sessions = MusicSession.index(user)
+ music_sessions.length.should == 0
+ end
+
+ it "does list a session if musician_access is set to false but user was invited" do
+ creator = FactoryGirl.create(:user)
+ session = FactoryGirl.create(:music_session, :creator => creator, :description => "Session", :musician_access => false)
+ user = FactoryGirl.create(:user)
+ FactoryGirl.create(:connection, :user => creator, :music_session => session)
+ FactoryGirl.create(:friendship, :user => creator, :friend => user)
+ FactoryGirl.create(:friendship, :user => user, :friend => creator)
+ FactoryGirl.create(:invitation, :sender => creator, :receiver => user, :music_session => session)
+
+ music_sessions = MusicSession.index(user)
+ music_sessions.length.should == 1
+ end
+
+ it "lists a session if the genre matches" do
+ creator = FactoryGirl.create(:user)
+ genre = FactoryGirl.create(:genre)
+ session = FactoryGirl.create(:music_session, :creator => creator, :description => "Session", :genres => [genre])
+ user = FactoryGirl.create(:user)
+
+ music_sessions = MusicSession.index(user, nil, [genre.id])
+ music_sessions.length.should == 1
+ end
+
+ it "does not list a session if the genre fails to match" do
+ creator = FactoryGirl.create(:user)
+ genre1 = FactoryGirl.create(:genre)
+ genre2 = FactoryGirl.create(:genre)
+ session = FactoryGirl.create(:music_session, :creator => creator, :description => "Session", :genres => [genre1])
+ user = FactoryGirl.create(:user)
+
+ music_sessions = MusicSession.index(user, nil, [genre2.id])
+ music_sessions.length.should == 0
+ end
+
+ it "does not list a session if friends_only is set and no friends are in it" do
+ creator = FactoryGirl.create(:user)
+ session = FactoryGirl.create(:music_session, :creator => creator, :description => "Session")
+ user = FactoryGirl.create(:user)
+
+ music_sessions = MusicSession.index(user, nil, nil, true)
+ music_sessions.length.should == 0
+ end
+
+ it "lists a session properly if a friend is in it" do
+ creator = FactoryGirl.create(:user)
+ session = FactoryGirl.create(:music_session, :creator => creator, :description => "Session")
+ user = FactoryGirl.create(:user)
+ FactoryGirl.create(:friendship, :user => creator, :friend => user)
+ FactoryGirl.create(:friendship, :user => user, :friend => creator)
+ FactoryGirl.create(:connection, :user => creator, :music_session => session)
+
+ music_sessions = MusicSession.index(user, nil, nil)
+ music_sessions.length.should == 1
+ music_sessions = MusicSession.index(user, nil, nil, true)
+ music_sessions.length.should == 1
+ music_sessions = MusicSession.index(user, nil, nil, false, true)
+ music_sessions.length.should == 0
+ music_sessions = MusicSession.index(user, nil, nil, true, true)
+ music_sessions.length.should == 1
+ end
+
+ it "does not list a session if my_bands_only is set and it's not my band" do
+ creator = FactoryGirl.create(:user)
+ session = FactoryGirl.create(:music_session, :creator => creator, :description => "Session")
+ user = FactoryGirl.create(:user)
+
+ music_sessions = MusicSession.index(user, nil, nil, false, true)
+ music_sessions.length.should == 0
+ end
+
+ it "lists a session properly if it's my band's session" do
+ band = FactoryGirl.create(:band)
+ creator = FactoryGirl.create(:user)
+ session = FactoryGirl.create(:music_session, :creator => creator, :description => "Session", :band => band)
+ user = FactoryGirl.create(:user)
+ FactoryGirl.create(:band_musician, :band => band, :user => creator)
+ FactoryGirl.create(:band_musician, :band => band, :user => user)
+
+ music_sessions = MusicSession.index(user, nil, nil)
+ music_sessions.length.should == 1
+ music_sessions = MusicSession.index(user, nil, nil, true)
+ music_sessions.length.should == 0
+ music_sessions = MusicSession.index(user, nil, nil, false, true)
+ music_sessions.length.should == 1
+ music_sessions = MusicSession.index(user, nil, nil, true, true)
+ music_sessions.length.should == 1
+ end
+
+ it "updates the fields of a music session properly" do
+ genre1 = FactoryGirl.create(:genre)
+ genre2 = FactoryGirl.create(:genre)
+ genre3 = FactoryGirl.create(:genre)
+ genre4 = FactoryGirl.create(:genre)
+ creator = FactoryGirl.create(:user)
+ session = FactoryGirl.create(:music_session, :creator => creator, :description => "Session", :genres => [genre3,genre4])
+ session.update_attributes({:description => "Session2", :genre => [genre1, genre2]})
+ session.genres = [genre1, genre2]
+ session.reload
+ session.description.should == "Session2"
+ session.genres.length.should == 2
+ session.genres[0].id.should == genre1.id
+ end
+
+=begin
+ # mslemmer:
+ # I'm going to clean this up into smaller tasks.
+ it 'can list sessions with appropriate sort order' do
+
+ user1 = FactoryGirl.create(:user)
+ user2 = FactoryGirl.create(:user)
+ user3 = FactoryGirl.create(:user)
+ user4 = FactoryGirl.create(:user)
+ user5 = FactoryGirl.create(:user)
+
+ music_session = FactoryGirl.create(:music_session, :creator => user1, :musician_access => false)
+ FactoryGirl.create(:connection, :user => user1, :music_session => music_session)
+
+ music_sessions = MusicSession.index(user2)
+ music_sessions.length.should == 0
+ music_session2 = FactoryGirl.create(:music_session, :creator => user3, :musician_access => true)
+ FactoryGirl.create(:connection, :user => user3, :music_session => music_session2)
+
+ music_sessions = MusicSession.index(user2)
+ music_sessions.length.should == 1
+ music_sessions[0].connections[0].user.friends.length == 0
+
+ # users 1 and 5 are friends
+ FactoryGirl.create(:friendship, :user => user1, :friend => user5)
+ FactoryGirl.create(:friendship, :user => user5, :friend => user1)
+
+ # users 1 and 2 are friends
+ FactoryGirl.create(:friendship, :user => user1, :friend => user2)
+ FactoryGirl.create(:friendship, :user => user2, :friend => user1)
+
+ # users 2 and 4 are friends
+ FactoryGirl.create(:friendship, :user => user2, :friend => user4)
+ FactoryGirl.create(:friendship, :user => user4, :friend => user2)
+
+ # user 2 should now be able to see this session, because his friend is in the session
+ music_sessions = MusicSession.index(user2)
+ music_sessions.length.should == 2
+ music_sessions[0].id.should == music_session.id
+ music_sessions[0].connections[0].user.id.should == user1.id
+ music_sessions[0].connections[0].user.friends.length == 1
+ music_sessions[1].id.should == music_session2.id
+
+ FactoryGirl.create(:invitation, :sender => user1, :receiver => user2, :music_session => music_session)
+
+ music_sessions = MusicSession.index(user2)
+ music_sessions.length.should == 2
+ music_sessions[0].id.should == music_session.id
+ music_sessions[1].id.should == music_session2.id
+
+ # create another, but friendy usic session with user 4
+ music_session3 = FactoryGirl.create(:music_session, :creator => user4, :musician_access => false, :created_at => 1.week.ago)
+ FactoryGirl.create(:connection, :user => user4, :music_session => music_session3)
+
+ music_sessions = MusicSession.index(user2)
+ music_sessions.length.should == 3
+ music_sessions[0].id.should == music_session.id
+ music_sessions[1].id.should == music_session3.id
+ music_sessions[2].id.should == music_session2.id
+
+ # verify we can inspect the data
+ music_session.invitations.length.should == 1
+
+
+ music_session4 = FactoryGirl.create(:music_session, :creator => user5, :musician_access => false, :created_at => 2.week.ago)
+ FactoryGirl.create(:connection, :user => user5, :music_session => music_session4)
+
+ music_sessions = MusicSession.index(user2)
+ music_sessions.length.should == 3
+ # make this session public now
+ music_session4.musician_access = true
+ music_session4.save
+
+ music_sessions = MusicSession.index(user2)
+ music_sessions.length.should == 4
+ music_sessions[0].id.should == music_session.id
+ music_sessions[1].id.should == music_session3.id
+ music_sessions[2].id.should == music_session2.id
+ music_sessions[3].id.should == music_session4.id
+
+ # ok let's make the public session that we just made, become a 'friendy' one
+ # make user2/5 friends
+ FactoryGirl.create(:friendship, :user => user2, :friend => user5)
+ FactoryGirl.create(:friendship, :user => user5, :friend => user2)
+
+ music_sessions = MusicSession.index(user2)
+ music_sessions.length.should == 4
+ music_sessions[0].id.should == music_session.id
+ music_sessions[1].id.should == music_session3.id
+ music_sessions[2].id.should == music_session4.id
+ music_sessions[3].id.should == music_session2.id
+
+ # and finally make it an invite
+ FactoryGirl.create(:invitation, :sender => user5, :receiver => user2, :music_session => music_session4 )
+ music_sessions = MusicSession.index(user2)
+ music_sessions.length.should == 4
+
+ music_sessions[0].id.should == music_session.id
+ music_sessions[1].id.should == music_session4.id
+ music_sessions[2].id.should == music_session3.id
+ music_sessions[3].id.should == music_session2.id
+ end
+=end
+
+ it 'uninvited users cant join approval-required sessions without invitation' do
+ user1 = FactoryGirl.create(:user) # in the jam session
+ user2 = FactoryGirl.create(:user) # in the jam session
+
+ music_session = FactoryGirl.create(:music_session, :creator => user1, :musician_access => true, :approval_required => true)
+
+ connection1 = FactoryGirl.create(:connection, :user => user1, :music_session => music_session)
+ expect { FactoryGirl.create(:connection, :user => user2, :music_session => music_session, :joining_session => true) }.to raise_error(ActiveRecord::RecordInvalid)
+
+ end
+
+ it "must have legal_terms accepted" do
+ user1 = FactoryGirl.create(:user)
+ music_session = FactoryGirl.build(:music_session, :creator => user1, :legal_terms=> false)
+ music_session.save
+ music_session.valid?.should be_false
+ music_session.errors["legal_terms"].should == ["is not included in the list"]
+ end
+
+ it "cannot have profanity in the description" do
+ user1 = FactoryGirl.create(:user)
+ music_session = FactoryGirl.build(:music_session, :creator => user1, :legal_terms=> false, :description => "fuck you")
+ music_session.save
+ music_session.valid?.should be_false
+ end
+
+end
+
diff --git a/ruby/spec/jam_ruby/models/recorded_track_spec.rb b/ruby/spec/jam_ruby/models/recorded_track_spec.rb
new file mode 100644
index 000000000..dcd374aaf
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/recorded_track_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe RecordedTrack do
+
+ before do
+ @user = FactoryGirl.create(:user)
+ @connection = FactoryGirl.create(:connection, :user => @user)
+ @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
+ @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
+ @recording = FactoryGirl.create(:recording, :owner => @user)
+ end
+
+ it "should copy from a regular track properly" do
+ @recorded_track = RecordedTrack.create_from_track(@track, @recording)
+
+ @recorded_track.user.id.should == @track.connection.user.id
+ @recorded_track.instrument.id.should == @track.instrument.id
+ @recorded_track.next_part_to_upload.should == 0
+ @recorded_track.fully_uploaded.should == false
+ end
+
+ it "should update the next part to upload properly" do
+ @recorded_track = RecordedTrack.create_from_track(@track, @recording)
+ @recorded_track.upload_part_complete(0)
+ @recorded_track.upload_part_complete(1)
+ @recorded_track.upload_part_complete(2)
+ @recorded_track.next_part_to_upload.should == 3
+ end
+
+
+ it "should error if the wrong part is uploaded" do
+ @recorded_track = RecordedTrack.create_from_track(@track, @recording)
+ @recorded_track.upload_part_complete(0)
+ @recorded_track.upload_part_complete(1)
+ expect { @recorded_track.upload_part_complete(3) }.to raise_error
+ @recorded_track.next_part_to_upload.should == 2
+ end
+
+ it "properly finds a recorded track given its upload filename" do
+ @recorded_track = RecordedTrack.create_from_track(@track, @recording)
+ RecordedTrack.find_by_upload_filename("recording_#{@recorded_track.id}").should == @recorded_track
+ end
+
+ it "gets a url for the track" do
+ @recorded_track = RecordedTrack.create_from_track(@track, @recording)
+ @recorded_track.url.should == S3Manager.url(S3Manager.hashed_filename("recorded_track", @recorded_track.id))
+ end
+
+
+end
+
+
diff --git a/ruby/spec/jam_ruby/models/recording_spec.rb b/ruby/spec/jam_ruby/models/recording_spec.rb
new file mode 100644
index 000000000..ed1ce9a4e
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/recording_spec.rb
@@ -0,0 +1,284 @@
+require 'spec_helper'
+
+describe Recording do
+
+ before do
+ S3Manager.set_unit_test
+ @user = FactoryGirl.create(:user)
+ @connection = FactoryGirl.create(:connection, :user => @user)
+ @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
+ @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
+ @music_session = FactoryGirl.create(:music_session, :creator => @user, :musician_access => true)
+ @music_session.connections << @connection
+ @music_session.save
+ end
+
+ it "should not start a recording if the music session doesnt exist" do
+ expect { Recording.start("bad_music_session_id", @user) }.to raise_error
+ end
+
+ it "should set up the recording properly when recording is started with 1 user in the session" do
+ @music_session.recording.should == nil
+ @recording = Recording.start(@music_session.id, @user)
+ @music_session.reload
+ @music_session.recording.should == @recording
+ @recording.owner_id.should == @user.id
+
+ @recorded_tracks = RecordedTrack.where(:recording_id => @recording.id)
+ @recorded_tracks.length.should == 1
+ @recorded_tracks.first.instrument_id == @track.instrument_id
+ @recorded_tracks.first.user_id == @track.connection.user_id
+ end
+
+ it "should not start a recording if the session is already being recorded" do
+ Recording.start(@music_session.id, @user)
+ expect { Recording.start(@music_session.id, @user) }.to raise_error
+ end
+
+ it "should return the state to normal properly when you stop a recording" do
+ @recording = Recording.start(@music_session.id, @user)
+ @recording.stop
+ @music_session.reload
+ @music_session.recording.should == nil
+ @recording.reload
+ @recording.music_session.should == nil
+ end
+
+
+ it "should error when you stop a recording twice" do
+ @recording = Recording.start(@music_session.id, @user)
+ @recording.stop
+ expect { @recording.stop }.to raise_error
+ end
+
+ it "should be able to start, stop then start a recording again for the same music session" do
+ @recording = Recording.start(@music_session.id, @user)
+ @recording.stop
+ @recording2 = Recording.start(@music_session.id, @user)
+ @music_session.recording.should == @recording2
+ end
+
+ it "should NOT attach the recording to all users in a the music session when recording started" do
+ @user2 = FactoryGirl.create(:user)
+ @connection2 = FactoryGirl.create(:connection, :user => @user2)
+ @instrument2 = FactoryGirl.create(:instrument, :description => 'a great instrument')
+ @track2 = FactoryGirl.create(:track, :connection => @connection2, :instrument => @instrument2)
+
+ @music_session.connections << @connection2
+
+ @recording = Recording.start(@music_session.id, @user)
+ @user.recordings.length.should == 0
+ #@user.recordings.first.should == @recording
+
+ @user2.recordings.length.should == 0
+ #@user2.recordings.first.should == @recording
+ end
+
+ it "should report correctly whether its tracks have been uploaded" do
+ @recording = Recording.start(@music_session.id, @user)
+ @recording.uploaded?.should == false
+ @recording.stop
+ @recording.reload
+ @recording.uploaded?.should == false
+ @recording.recorded_tracks.first.fully_uploaded = true
+ @recording.uploaded?.should == true
+ end
+
+ it "should destroy a recording and all its recorded tracks properly" do
+ @recording = Recording.start(@music_session.id, @user)
+ @recording.stop
+ @recording.reload
+ @recorded_track = @recording.recorded_tracks.first
+ @recording.destroy
+ expect { Recording.find(@recording.id) }.to raise_error
+ expect { RecordedTracks.find(@recorded_track.id) }.to raise_error
+ end
+
+ it "should allow a user to claim a recording" do
+ @recording = Recording.start(@music_session.id, @user)
+ @recording.stop
+ @recording.reload
+ @genre = FactoryGirl.create(:genre)
+ @recording.claim(@user, "name", @genre, true, true)
+ @recording.reload
+ @recording.users.length.should == 1
+ @recording.users.first.should == @user
+ @user.recordings.length.should == 1
+ @user.recordings.first.should == @recording
+ @recording.claimed_recordings.length.should == 1
+ @claimed_recording = @recording.claimed_recordings.first
+ @claimed_recording.name.should == "name"
+ @claimed_recording.genre.should == @genre
+ @claimed_recording.is_public.should == true
+ @claimed_recording.is_downloadable.should == true
+ end
+
+ it "should fail if a user who was not in the session claims a recording" do
+ @recording = Recording.start(@music_session.id, @user)
+ @recording.stop
+ @recording.reload
+ user2 = FactoryGirl.create(:user)
+ expect { @recording.claim(user2) }.to raise_error
+ end
+
+ it "should fail if a user tries to claim a recording twice" do
+ @recording = Recording.start(@music_session.id, @user)
+ @recording.stop
+ @recording.reload
+ @genre = FactoryGirl.create(:genre)
+ @recording.claim(@user, "name", @genre, true, true)
+ @recording.reload
+ expect { @recording.claim(@user, "name", @genre, true, true) }.to raise_error
+ end
+
+ it "should allow editing metadata for claimed recordings" do
+ @recording = Recording.start(@music_session.id, @user)
+ @recording.stop
+ @recording.reload
+ @genre = FactoryGirl.create(:genre)
+ @claimed_recording = @recording.claim(@user, "name", @genre, true, true)
+ @genre2 = FactoryGirl.create(:genre)
+ @claimed_recording.update_fields(@user, :name => "name2", :genre => @genre2.id, :is_public => false, :is_downloadable => false)
+ @claimed_recording.reload
+ @claimed_recording.name.should == "name2"
+ @claimed_recording.genre.should == @genre2
+ @claimed_recording.is_public.should == false
+ @claimed_recording.is_downloadable.should == false
+ end
+
+ it "should only allow the owner to edit a claimed recording" do
+ @recording = Recording.start(@music_session.id, @user)
+ @recording.stop
+ @recording.reload
+ @genre = FactoryGirl.create(:genre)
+ @claimed_recording = @recording.claim(@user, "name", @genre, true, true)
+ @user2 = FactoryGirl.create(:user)
+ expect { @claimed_recording.update_fields(@user2, "name2") }.to raise_error
+ end
+
+ it "should record the duration of the recording properly" do
+ @recording = Recording.start(@music_session.id, @user)
+ @recording.duration.should be_nil
+ @recording.stop
+ @recording.reload
+ @recording.duration.should_not be_nil
+ # Note: it will be 0 since this was fast. You can see something non-zero by just
+ # inserting a sleep here.
+ # puts @recording.duration
+ end
+
+ it "should only destroy a single claimed_recording if there are more than one" do
+ @user2 = FactoryGirl.create(:user)
+ @connection2 = FactoryGirl.create(:connection, :user => @user2)
+ @track = FactoryGirl.create(:track, :connection => @connection2, :instrument => @instrument)
+ @music_session.connections << @connection2
+ @music_session.save
+ @recording = Recording.start(@music_session.id, @user)
+ @recording.stop
+ @recording.reload
+ @genre = FactoryGirl.create(:genre)
+ @claimed_recording = @recording.claim(@user, "name", @genre, true, true)
+ expect { @claimed_recordign.discard(@user2) }.to raise_error
+ @claimed_recording = @recording.claim(@user2, "name2", @genre, true, true)
+ @claimed_recording.discard(@user2)
+ @recording.reload
+ @recording.claimed_recordings.length.should == 1
+ end
+
+ it "should destroy the entire recording if there was only one claimed_recording which is discarded" do
+ @recording = Recording.start(@music_session.id, @user)
+ @recording.stop
+ @recording.reload
+ @genre = FactoryGirl.create(:genre)
+ @claimed_recording = @recording.claim(@user, "name", @genre, true, true)
+ @claimed_recording.discard(@user)
+ expect { Recording.find(@recording.id) }.to raise_error
+ expect { ClaimedRecording.find(@claimed_recording.id) }.to raise_error
+ end
+
+ it "should return a file list for a user properly" do
+ @recording = Recording.start(@music_session.id, @user)
+ @recording.stop
+ @recording.reload
+ @genre = FactoryGirl.create(:genre)
+ @recording.claim(@user, "Recording", @genre, true, true)
+ Recording.list(@user)["downloads"].length.should == 0
+ Recording.list(@user)["uploads"].length.should == 1
+ file = Recording.list(@user)["uploads"].first
+ @recorded_track = @recording.recorded_tracks.first
+ file.should == @recorded_track.filename
+ @recorded_track.upload_start(25000, "md5hash")
+ @recorded_track.upload_complete
+ Recording.list(@user)["downloads"].length.should == 1
+ Recording.list(@user)["uploads"].length.should == 0
+ file = Recording.list(@user)["downloads"].first
+ file[:type].should == "recorded_track"
+ file[:id].should == @recorded_track.id
+ file[:length].should == 25000
+ file[:md5].should == "md5hash"
+ file[:url].should == S3Manager.url(S3Manager.hashed_filename('recorded_track', @recorded_track.id))
+
+ # Note that the recording should automatically schedule a mix when the upload completes
+ @recording.mixes.length.should == 1
+ @mix = Mix.next('server')
+ @mix.should_not be_nil
+ @mix.finish(50000, "md5hash")
+ Recording.list(@user)["downloads"].length.should == 2
+ Recording.list(@user)["uploads"].length.should == 0
+ file = Recording.list(@user)["downloads"].last
+ file[:type].should == "mix"
+ file[:id].should == @mix.id
+ file[:length].should == 50000
+ file[:md5].should == "md5hash"
+ file[:url].should == S3Manager.url(S3Manager.hashed_filename('mix', @mix.id))
+
+ end
+
+ it "should create a base mix manifest properly" do
+ @user2 = FactoryGirl.create(:user)
+ @connection2 = FactoryGirl.create(:connection, :user => @user)
+ @instrument2 = FactoryGirl.create(:instrument, :description => 'a great instrument')
+ @track2 = FactoryGirl.create(:track, :connection => @connection2, :instrument => @instrument2)
+ @music_session.connections << @connection2
+ @music_session.save
+ @recording = Recording.start(@music_session.id, @user)
+ #sleep 4
+ @recording.stop
+ @recording.recorded_tracks.length.should == 2
+ @recorded_track = @recording.recorded_tracks.first
+ @recorded_track.upload_start(25000, "md5hash")
+ @recorded_track.upload_complete
+ @recorded_track2 = @recording.recorded_tracks.last
+ @recorded_track2.upload_start(50000, "md5hash2")
+ @recorded_track2.upload_complete
+ mix_manifest = @recording.base_mix_manifest
+ mix_manifest.should_not be_nil
+ files = mix_manifest["files"]
+ files.should_not be_nil
+ files.length.should == 2
+ files.first["codec"].should == "vorbis"
+ files.first["offset"].should == 0
+ files.first["url"].should == @recording.recorded_tracks.first.url
+ files.last["codec"].should == "vorbis"
+ files.last["offset"].should == 0
+ files.last["url"].should == @recording.recorded_tracks.last.url
+
+ timeline = mix_manifest["timeline"]
+ timeline.should_not be_nil
+ timeline.length.should == 2
+ timeline.first["timestamp"].should == 0
+ timeline.first["end"].should be_nil
+ mix = timeline.first["mix"]
+ mix.should_not be_nil
+ mix.length.should == 2
+ mix.first["balance"].should == 0
+ mix.first["level"].should == 100
+ mix.last["balance"].should == 0
+ mix.last["level"].should == 100
+
+ timeline.last["timestamp"].should == @recording.duration
+ timeline.last["end"].should == true
+ end
+end
+
+
diff --git a/ruby/spec/jam_ruby/models/search_spec.rb b/ruby/spec/jam_ruby/models/search_spec.rb
new file mode 100644
index 000000000..b0798c570
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/search_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe Search do
+
+ before(:each) do
+ end
+
+
+ def create_peachy_data
+ @user = FactoryGirl.create(:user, first_name: "Peach", last_name: "Pit", email: "user@example.com", musician: true, city: "Apex", state: "NC", country:"USA")
+ @fan = FactoryGirl.create(:user, first_name: "Peach Peach", last_name: "Pit", email: "fan@example.com", musician: false, city: "Apex", state: "NC", country:"USA")
+ @band = FactoryGirl.create(:band, name: "Peach pit", website: "www.bands.com", biography: "zomg we rock", city: "Apex", state: "NC", country:"USA")
+ end
+
+ def assert_peachy_data
+ search = Search.search('peach')
+
+ search.recordings.length.should == 0
+ search.bands.length.should == 1
+ search.musicians.length.should == 1
+ search.fans.length.should == 1
+
+ musician = search.musicians[0]
+ musician.should be_a_kind_of User
+ musician.id.should == @user.id
+
+ band = search.bands[0]
+ band.should be_a_kind_of Band
+ band.id.should == @band.id
+
+ fan = search.fans[0]
+ fan.should be_a_kind_of User
+ fan.id.should == @fan.id
+ end
+
+ it "search for band & musician " do
+ create_peachy_data
+
+ assert_peachy_data
+ end
+
+end
diff --git a/ruby/spec/jam_ruby/models/user_search_spec.rb b/ruby/spec/jam_ruby/models/user_search_spec.rb
new file mode 100644
index 000000000..486b955b2
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/user_search_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+describe User do
+
+ before(:each) do
+ @user = FactoryGirl.create(:user, first_name: "Example", last_name: "User", email: "user@example.com",
+ password: "foobar", password_confirmation: "foobar", musician: true, email_confirmed: true,
+ city: "Apex", state: "NC", country: "USA")
+ end
+
+ it "should allow search of one user" do
+ ws = User.search("Example User")
+ ws.length.should == 1
+ user_result = ws[0]
+ user_result.first_name.should == @user.first_name
+ user_result.last_name.should == @user.last_name
+ user_result.id.should == @user.id
+ user_result.location.should == @user.location
+ user_result.musician.should == true
+ end
+
+ it "should delete user" do
+ ws = User.search("Example User")
+ ws.length.should == 1
+ user_result = ws[0]
+ user_result.id.should == @user.id
+
+ @user.destroy
+
+ ws = User.search("Example User")
+ ws.length.should == 0
+ end
+
+ it "should update user" do
+ ws = User.search("Example User")
+ ws.length.should == 1
+ user_result = ws[0]
+ user_result.id.should == @user.id
+
+ @user.first_name = "bonus-junk"
+ @user.last_name = "more-junk"
+ @user.save
+
+ ws = User.search("Example User")
+ ws.length.should == 0
+
+ ws = User.search("Bonus")
+ ws.length.should == 1
+ user_result = ws[0]
+ user_result.id.should == @user.id
+ user_result.first_name.should == "bonus-junk"
+ end
+
+ it "should tokenize correctly" do
+ @user2 = FactoryGirl.create(:user, first_name: "peaches", last_name: "test", email: "peach@example.com",
+ password: "foobar", password_confirmation: "foobar", musician: true, email_confirmed: true,
+ city: "Apex", state: "NC", country: "USA")
+ ws = User.search("pea")
+ ws.length.should == 1
+ user_result = ws[0]
+ user_result.id.should == @user2.id
+ end
+
+ it "users who have signed up, but not confirmed should show up in search index due to VRFS-378" do
+ @user3 = FactoryGirl.create(:user, first_name: "unconfirmed", last_name: "unconfirmed", email: "unconfirmed@example.com",
+ password: "foobar", password_confirmation: "foobar", musician: true, email_confirmed: false,
+ city: "Apex", state: "NC", country: "USA")
+ ws = User.search("unconfirmed")
+ ws.length.should == 1
+
+ # Ok, confirm the user, and see them show up
+ @user3.email_confirmed = true
+ @user3.save
+
+ ws = User.search("unconfirmed")
+ ws.length.should == 1
+ user_result = ws[0]
+ user_result.id.should == @user3.id
+ 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
new file mode 100644
index 000000000..a0f7cd418
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/user_spec.rb
@@ -0,0 +1,419 @@
+require 'spec_helper'
+
+RESET_PASSWORD_URL = "/reset_token"
+
+describe User do
+
+ before do
+ @user = User.new(first_name: "Example", last_name: "User", email: "user@example.com",
+ password: "foobar", password_confirmation: "foobar", city: "Apex", state: "NC", country: "USA", terms_of_service: true, musician: true)
+ @user.musician_instruments << FactoryGirl.build(:musician_instrument, user: @user)
+ end
+
+ subject { @user }
+
+ it { should respond_to(:first_name) }
+ it { should respond_to(:last_name) }
+ it { should respond_to(:email) }
+ it { should respond_to(:password) }
+ it { should respond_to(:password_confirmation) }
+ it { should respond_to(:remember_token) }
+ it { should respond_to(:admin) }
+ it { should respond_to(:valid_password?) }
+ it { should respond_to(:can_invite) }
+
+ it { should be_valid }
+ it { should_not be_admin }
+
+ describe "accessible attributes" do
+ it "should not allow access to admin" do
+ userish = User.new(admin: true)
+ userish.admin.should == false # the .new style above will be ignored
+ userish.admin = true # but deliberate property setting will work
+ userish.admin.should == true
+ end
+ end
+
+ describe "with admin attribute set to 'true'" do
+ before do
+ @user.save!
+ @user.toggle!(:admin)
+ end
+
+ it { should be_admin }
+ end
+
+
+ describe "when first name is not present" do
+ before { @user.first_name = " " }
+ it { should_not be_valid }
+ end
+
+ describe "when last name is not present" do
+ before { @user.last_name = " " }
+ it { should_not be_valid }
+ end
+
+ describe "when email is not present" do
+ before { @user.email = " " }
+ it { should_not be_valid }
+ end
+
+ describe "when first name is too long" do
+ before { @user.first_name = "a" * 51 }
+ it { should_not be_valid }
+ end
+
+ describe "when last name is too long" do
+ before { @user.last_name = "a" * 51 }
+ it { should_not be_valid }
+ 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
+ @user.should_not be_valid
+ end
+ end
+
+ describe "when email format is invalid" do
+ it "should be invalid" do
+ addresses = %w[user@foo,com user_at_foo.org example.user@foo.]
+ addresses.each do |invalid_address|
+ @user.email = invalid_address
+ @user.should_not be_valid
+ end
+ end
+ end
+
+ describe "when email format is valid" do
+ it "should be valid" do
+ addresses = %w[user@foo.COM A_US-ER@f.b.org frst.lst@foo.jp a+b@baz.cn]
+ addresses.each do |valid_address|
+ @user.email = valid_address
+ @user.should be_valid
+ end
+ end
+ end
+
+ describe "when email address is already taken" do
+ before do
+ user_with_same_email = @user.dup
+ user_with_same_email.email = @user.email.upcase
+ user_with_same_email.save
+ end
+
+ it { should_not be_valid }
+ end
+
+ describe "email address with mixed case" do
+ let(:mixed_case_email) { "Foo@ExAMPle.CoM" }
+
+
+ it "should be saved as all lower-case" do
+ @user.email = mixed_case_email
+ @user.save!
+ @user.reload.email.should == mixed_case_email
+ end
+ end
+
+ describe "when password is not present" do
+ before { @user.password = @user.password_confirmation = " " }
+ it { should_not be_valid }
+ end
+
+ describe "when password doesn't match confirmation" do
+ before { @user.password_confirmation = "mismatch" }
+ it { should_not be_valid }
+ end
+
+ describe "when password confirmation is nil" do
+ before { @user.password_confirmation = nil }
+ it { should_not be_valid }
+ end
+
+ describe "with a password that's too short" do
+ before { @user.password = @user.password_confirmation = "a" * 5 }
+ it { should be_invalid }
+ end
+
+
+ describe "set_password" do
+ before do
+ @user.confirm_email!
+ @user.save.should be_true
+ UserMailer.deliveries.clear
+ end
+
+ it "setting a new password should work" do
+ @user.set_password("foobar", "newpassword", "newpassword")
+ 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
+ @user.errors[:current_password].length.should == 1
+ UserMailer.deliveries.length.should == 0
+ end
+
+ it "setting a new password should fail if new ones dont match" do
+ @user.set_password("foobar", "newpassword", "newpassword2")
+ @user.errors.any?.should be_true
+ @user.errors[:password].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
+ @user.errors[:password].length.should == 1
+ UserMailer.deliveries.length.should == 0
+ end
+
+ it "setting a new password should fail if the new one is null" do
+ @user.set_password("foobar", nil, nil)
+ @user.errors.any?.should be_true
+ @user.errors[:password].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
+ end
+
+ it "assigns a reset_token and reset_token_created on reset" do
+ User.reset_password(@user.email, RESET_PASSWORD_URL)
+ @user.reload
+ @user.reset_password_token.should_not be_nil
+ @user.reset_password_token_created.should_not be_nil
+ @user.reset_password_token_created.should <= Time.now
+ @user.reset_password_token_created.should >= Time.now - 1.minute
+ end
+
+ it "errors if the wrong token comes in" do
+ User.reset_password(@user.email, RESET_PASSWORD_URL)
+ @user.reload
+ expect { User.set_password_from_token(@user.email, "wrongtoken", "newpassword", "newpassword") }.to raise_error
+ end
+
+ it "changes the password if the token is right" do
+ User.reset_password(@user.email, RESET_PASSWORD_URL)
+ @user.reload
+ User.set_password_from_token(@user.email, @user.reset_password_token, "newpassword", "newpassword")
+ User.authenticate(@user.email, "newpassword").should_not be_nil
+ @user.reload
+ end
+ end
+
+ describe "return value of authenticate method" do
+ before { @user.save }
+ let(:found_user) { User.find_by_email(@user.email) }
+
+ describe "with valid password" do
+ it { found_user.valid_password?(@user.password).should be_true }
+ end
+
+ describe "with invalid password" do
+ let(:user_for_invalid_password) { found_user.valid_password?("invalid") }
+
+ it { should_not == user_for_invalid_password }
+ specify { user_for_invalid_password.should be_false }
+ end
+ end
+
+ describe "remember token" do
+ before { @user.save }
+ its(:remember_token) { should_not be_blank }
+ end
+
+ 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
+
+ describe "with invalid password" do
+ it { User.authenticate(@user.email, "invalid").should be_nil }
+ end
+
+ describe "with invalid email" do
+ it { User.authenticate("junk", "invalid").should be_nil }
+ end
+
+ describe "with nil args" do
+ it { User.authenticate(nil, nil).should be_nil }
+ end
+
+ describe "with empty args" do
+ it { User.authenticate("", "").should be_nil }
+ end
+ end
+
+ describe "create_dev_user" do
+ before { @dev_user = User.create_dev_user("Seth", "Call", "seth@jamkazam.com", "Jam123", "Austin", "Texas", "USA", nil, nil) }
+
+ subject { @dev_user }
+
+ describe "creates a valid record" do
+ it { should be_valid }
+ end
+
+ 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", "USA", nil, nil) }
+
+ it { should be_valid }
+
+ its(:last_name) { should == "Call2" }
+
+ end
+ end
+
+ describe "update email" do
+
+ before do
+ UserMailer.deliveries.clear
+ end
+
+ describe "begin email update" do
+ describe "success" do
+ before do
+ @user.begin_update_email("somenewemail@blah.com", "foobar", "http://www.jamkazam.com/confirm_email_update?token=")
+ end
+
+ # useful to see contents of email without actually running the app and sending it
+ it { @user.errors.any?.should be_false }
+ it { @user.update_email.should == "somenewemail@blah.com" }
+ it { @user.update_email_confirmation_url.should == "http://www.jamkazam.com/confirm_email_update?token=#{@user.update_email_token}" }
+ it { UserMailer.deliveries.length.should == 1 }
+
+ end
+
+ it "no email on error" do
+ @user.begin_update_email("somenewemail@blah.com", "wrong password", "http://www.jamkazam.com/confirm_email_update?token=")
+
+ UserMailer.deliveries.length.should == 0
+ end
+
+ it "bad password validation" do
+ @user.begin_update_email("somenewemail@blah.com", "wrong password", "http://www.jamkazam.com/confirm_email_update?token=")
+
+ @user.errors[:current_password][0].should == ValidationMessages::NOT_YOUR_PASSWORD
+ end
+
+ it "matches current email" do
+ @user.begin_update_email(@user.email, "foobar", "http://www.jamkazam.com/confirm_email_update?token=")
+
+ @user.errors[:update_email][0].should == ValidationMessages::EMAIL_MATCHES_CURRENT
+ end
+
+ it "existing email of another user" do
+ another_user = FactoryGirl.create(:user)
+ @user.begin_update_email(another_user.email, "foobar", "http://www.jamkazam.com/confirm_email_update?token=")
+
+ @user.errors[:update_email][0].should == ValidationMessages::EMAIL_ALREADY_TAKEN
+ end
+
+ it "bogus email" do
+ @user.begin_update_email("not_an_email", "foobar", "http://www.jamkazam.com/confirm_email_update?token=")
+
+ @user.errors[:update_email][0].should == "is invalid"
+ end
+
+ it "empty email" do
+ @user.begin_update_email(nil, "foobar", "http://www.jamkazam.com/confirm_email_update?token=")
+
+ @user.errors[:update_email][0].should == "can't be blank"
+ end
+ end
+
+ describe "finalize email update" do
+ before do
+ @user.begin_update_email("somenewemail@blah.com", "foobar", "http://www.jamkazam.com/confirm_email_update?token=")
+ UserMailer.deliveries.clear
+ end
+
+ describe "success" do
+
+ before do
+ @finalized = User.finalize_update_email(@user.update_email_token)
+ end
+
+ it { @finalized.should == @user }
+ it { @finalized.email.should == "somenewemail@blah.com" }
+ it { UserMailer.deliveries.length.should == 1 }
+ end
+
+ it "no email on unsuccessful finalize" do
+ expect { User.finalize_update_email("wrong_token") }.to raise_error(ActiveRecord::RecordNotFound)
+ UserMailer.deliveries.length.should == 0
+ end
+
+ it "bad token" do
+ expect { User.finalize_update_email("wrong_token") }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it "empty token" do
+ expect { User.finalize_update_email(nil) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+
+=begin
+ describe "update avatar" do
+
+ describe "success" do
+
+ let(:s3_path) { "/public/avatars/#{@user.id}/avatar.jpg" }
+ let(:original) { { "url" => "http://filepicker.io/blah", "key" => "/public/avatars/#{@user.id}/originals/avatar.jpg" } }
+ let(:clipped) { { "url" => "http://filepicker.io/blah", "key" => s3_path } }
+
+ before(:each) do
+ @user.update_avatar(original, clipped, "jamkazam")
+ end
+
+ it { @user.errors.any?.should be_false }
+ it { @user.original_fpfile.class == String }
+ it { @user.cropped_fpfile.class == String }
+ it { @user.photo_url = S3Util.url("jamkazam", s3_path, :secure => false ) }
+ end
+
+ describe "bad fpfiles" do
+ let(:s3_path) { "/public/avatars/#{@user.id}/avatar.jpg" }
+ let(:original) { { "url" => "http://filepicker.io/blah" } } # take out 'key', which is required by model
+ let(:clipped) { { "url" => "http://filepicker.io/blah", } } # take out 'key', which is required by model
+
+ before(:each) do
+ @user.update_avatar(original, clipped, "jamkazam")
+ end
+
+ it { @user.errors.any?.should be_true }
+ it { @user.errors[:original_fpfile][0].should == ValidationMessages::INVALID_FPFILE }
+ it { @user.errors[:cropped_fpfile][0].should == ValidationMessages::INVALID_FPFILE }
+ end
+
+ end
+=end
+
+end
diff --git a/ruby/spec/jam_ruby/mq_router_spec.rb b/ruby/spec/jam_ruby/mq_router_spec.rb
new file mode 100644
index 000000000..2e1b36750
--- /dev/null
+++ b/ruby/spec/jam_ruby/mq_router_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe MQRouter do
+
+ before do
+ @mq_router = MQRouter.new()
+ end
+
+ it "user_publish_to_session works (but faking MQ)" do
+
+ user1 = FactoryGirl.create(:user) # in the jam session
+ user2 = FactoryGirl.create(:user) # in the jam session
+
+ music_session = FactoryGirl.create(:music_session, :creator => user1)
+
+ music_session_member1 = FactoryGirl.create(:connection, :user => user1, :music_session => music_session, :ip_address => "1.1.1.1", :client_id => "1")
+ music_session_member2 = FactoryGirl.create(:connection, :user => user2, :music_session => music_session, :ip_address => "2.2.2.2", :client_id => "2")
+
+ @mq_router.should_receive(:publish_to_session).with(music_session.id, [music_session_member2.client_id], "a message", :client_id => music_session_member1.client_id)
+
+ @mq_router.user_publish_to_session(music_session, user1, "a message" ,:client_id => music_session_member1.client_id)
+ end
+
+ it "user_publish_to_session works (checking exchange callbacks)" do
+
+ user1 = FactoryGirl.create(:user) # in the jam session
+ user2 = FactoryGirl.create(:user) # in the jam session
+
+ music_session = FactoryGirl.create(:music_session, :creator => user1)
+
+ music_session_member1 = FactoryGirl.create(:connection, :user => user1, :music_session => music_session, :ip_address => "1.1.1.1", :client_id => "1")
+ music_session_member2 = FactoryGirl.create(:connection, :user => user2, :music_session => music_session, :ip_address => "2.2.2.2", :client_id => "2")
+
+ EM.run do
+
+ # mock up exchange
+ MQRouter.client_exchange = double("client_exchange")
+
+ MQRouter.client_exchange.should_receive(:publish).with("a message", :routing_key => "client.#{music_session_member2.client_id}")
+
+ @mq_router.user_publish_to_session(music_session, user1, "a message", :client_id => music_session_member1.client_id)
+
+ EM.stop
+ end
+ end
+
+end
diff --git a/ruby/spec/mailers/render_emails_spec.rb b/ruby/spec/mailers/render_emails_spec.rb
new file mode 100644
index 000000000..120a660f5
--- /dev/null
+++ b/ruby/spec/mailers/render_emails_spec.rb
@@ -0,0 +1,73 @@
+# The purpose of this 'test' is to render emails to disk,
+# So that a developer can look in tmp/emails and open them up and verify that they look OK
+# Also, to have Jenkins archive them to make it easier to check if a build look OK
+
+require "spec_helper"
+
+describe "RenderMailers", :slow => true do
+
+ let(:user) { FactoryGirl.create(:user) }
+
+ before(:each) do
+ @filename = nil # set this on your test to pin the filename; i just make it the name of the mailer method responsible for sending the mail
+ end
+
+ describe "UserMailer emails" do
+
+ before(:each) do
+ user.update_email = "my_new_email@jamkazam.com"
+ UserMailer.deliveries.clear
+ end
+
+ after(:each) do
+ UserMailer.deliveries.length.should == 1
+ mail = UserMailer.deliveries[0]
+ save_emails_to_disk(mail, @filename)
+ end
+
+ it { @filename="welcome_message"; UserMailer.welcome_message(user, "/signup").deliver }
+ it { @filename="password_reset"; UserMailer.password_reset(user, '/reset_password').deliver }
+ it { @filename="password_changed"; UserMailer.password_changed(user).deliver }
+ it { @filename="updated_email"; UserMailer.updated_email(user).deliver }
+ it { @filename="updating_email"; UserMailer.updating_email(user).deliver }
+ end
+
+ describe "InvitedUserMailer emails" do
+
+ let(:user2) { FactoryGirl.create(:user) }
+ let(:invited_user) { FactoryGirl.create(:invited_user, :sender => user2) }
+ let(:admin_invited_user) { FactoryGirl.create(:invited_user) }
+
+ before(:each) do
+ InvitedUserMailer.deliveries.clear
+ end
+
+ after(:each) do
+ UserMailer.deliveries.length.should == 2
+ # NOTE! we take the second email, because the act of creating the InvitedUser model
+ # sends an email too, before our it {} block runs. This is because we have an InvitedUserObserver
+ mail = InvitedUserMailer.deliveries[1]
+ save_emails_to_disk(mail, @filename)
+ end
+
+ it { @filename="friend_invitation"; InvitedUserMailer.deliveries.clear; InvitedUserMailer.friend_invitation(invited_user).deliver }
+ it { @filename="welcome_betauser"; InvitedUserMailer.welcome_betauser(admin_invited_user).deliver }
+ end
+
+end
+
+def save_emails_to_disk(mail, filename)
+ # taken from: https://github.com/originalpete/actionmailer_extensions/blob/master/lib/actionmailer_extensions.rb
+ # this extension does not work with ActionMailer 3.x, but this method is all we need
+
+ if filename.nil?
+ filename = mail.subject
+ end
+
+ 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|
+ f << mail.encoded
+ }
+end
\ No newline at end of file
diff --git a/ruby/spec/mailers/user_mailer_spec.rb b/ruby/spec/mailers/user_mailer_spec.rb
new file mode 100644
index 000000000..68d7da550
--- /dev/null
+++ b/ruby/spec/mailers/user_mailer_spec.rb
@@ -0,0 +1,121 @@
+require "spec_helper"
+
+###########################################################
+# We test just the mailer templating here. #
+# In other words, make sure there are no glaring oopsies, #
+# such as broken templates, or sending to wrong address #
+###########################################################
+
+
+describe UserMailer do
+
+ let(:user) { FactoryGirl.create(:user) }
+
+ before(:each) do
+ UserMailer.deliveries.clear
+ end
+
+
+
+ describe "should send welcome email" do
+
+ let (:mail) { UserMailer.deliveries[0] }
+ let (:signup_confirmation_url) { "http://example.com/confirm" }
+ let (:signup_confirmation_url_with_token ) { "#{signup_confirmation_url}/#{user.signup_token}" }
+
+ before(:each) do
+ UserMailer.welcome_message(user, signup_confirmation_url_with_token).deliver
+ end
+
+
+ it { UserMailer.deliveries.length.should == 1 }
+ it { mail['from'].to_s.should == UserMailer::DEFAULT_SENDER }
+ it { mail['to'].to_s.should == user.email }
+ it { mail.multipart?.should == true } # because we send plain + html
+
+ # verify that the messages are correctly configured
+ it { mail.html_part.body.include?("Welcome").should be_true }
+ it { mail.html_part.body.include?(signup_confirmation_url_with_token).should be_true }
+ it { mail.text_part.body.include?("Welcome").should be_true }
+ it { mail.text_part.body.include?(signup_confirmation_url_with_token).should be_true }
+ end
+
+ describe "should send reset password" do
+
+ let(:mail) { UserMailer.deliveries[0] }
+ before(:each) do
+ UserMailer.password_reset(user, '/reset_password').deliver
+ end
+
+
+ it { UserMailer.deliveries.length.should == 1 }
+
+ it { mail['from'].to_s.should == UserMailer::DEFAULT_SENDER }
+ it { mail['to'].to_s.should == user.email }
+ it { mail.multipart?.should == true } # because we send plain + html
+
+ # verify that the messages are correctly configured
+ it { mail.html_part.body.include?("Reset").should be_true }
+ it { mail.text_part.body.include?("Reset").should be_true }
+ end
+
+ describe "should send change password confirmation" do
+
+ let(:mail) { UserMailer.deliveries[0] }
+
+ before(:each) do
+ UserMailer.password_changed(user).deliver
+ end
+
+ it { UserMailer.deliveries.length.should == 1 }
+
+ it { mail['from'].to_s.should == UserMailer::DEFAULT_SENDER }
+ it { mail['to'].to_s.should == user.email }
+ it { mail.multipart?.should == true } # because we send plain + html
+
+ # verify that the messages are correctly configured
+ it { mail.html_part.body.include?("changed your password").should be_true }
+ it { mail.text_part.body.include?("changed your password").should be_true }
+ end
+
+ describe "should send update email confirmation" do
+
+ let(:mail) { UserMailer.deliveries[0] }
+
+ before(:each) do
+ UserMailer.updated_email(user).deliver
+ end
+
+ it { UserMailer.deliveries.length.should == 1 }
+
+ it { mail['from'].to_s.should == UserMailer::DEFAULT_SENDER }
+ it { mail['to'].to_s.should == user.email }
+ it { mail.multipart?.should == true } # because we send plain + html
+
+ # verify that the messages are correctly configured
+ it { mail.html_part.body.include?("#{user.email} has been confirmed as your new email address.").should be_true }
+ it { mail.text_part.body.include?("#{user.email} has been confirmed as your new email address.").should be_true }
+ end
+
+ describe "should send updating email" do
+
+ let(:mail) { UserMailer.deliveries[0] }
+
+ before(:each) do
+ user.update_email = "my_new_email@jamkazam.com"
+ UserMailer.updating_email(user).deliver
+ end
+
+ it { UserMailer.deliveries.length.should == 1 }
+
+ it { mail['from'].to_s.should == UserMailer::DEFAULT_SENDER }
+ it { mail['to'].to_s.should == user.update_email }
+ it { mail.multipart?.should == true } # because we send plain + html
+
+ # verify that the messages are correctly configured
+ it { mail.html_part.body.include?("to confirm your change in email").should be_true }
+ it { mail.text_part.body.include?("to confirm your change in email").should be_true }
+ end
+
+
+end
diff --git a/ruby/spec/spec_db.rb b/ruby/spec/spec_db.rb
new file mode 100644
index 000000000..b825973f5
--- /dev/null
+++ b/ruby/spec/spec_db.rb
@@ -0,0 +1,12 @@
+class SpecDb
+
+ TEST_DB_NAME="jam_ruby_test"
+
+ TEST_USER_ID = "1" #test@jamkazam.com
+ def self.recreate_database
+ conn = PG::Connection.open("dbname=postgres user=postgres password=postgres host=localhost")
+ conn.exec("DROP DATABASE IF EXISTS #{TEST_DB_NAME}")
+ conn.exec("CREATE DATABASE #{TEST_DB_NAME}")
+ JamDb::Migrator.new.migrate(:dbname => TEST_DB_NAME, :user => "postgres", :password => "postgres", :host => "localhost")
+ end
+end
diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb
new file mode 100644
index 000000000..71905ef15
--- /dev/null
+++ b/ruby/spec/spec_helper.rb
@@ -0,0 +1,95 @@
+
+require 'active_record'
+require 'jam_db'
+require 'spec_db'
+require 'uses_temp_files'
+
+# recreate test database and migrate it
+SpecDb::recreate_database
+
+# initialize ActiveRecord's db connection
+ActiveRecord::Base.establish_connection(YAML::load(File.open('config/database.yml'))["test"])
+
+require 'jam_ruby'
+require 'factory_girl'
+require 'rubygems'
+require 'spork'
+require 'database_cleaner'
+require 'factories'
+
+# uncomment this to see active record logs
+# ActiveRecord::Base.logger = Logger.new(STDOUT) if defined?(ActiveRecord::Base)
+
+include JamRuby
+
+# manually register observers
+ActiveRecord::Base.add_observer InvitedUserObserver.instance
+ActiveRecord::Base.add_observer UserObserver.instance
+ActiveRecord::Base.add_observer FeedbackObserver.instance
+
+
+# put ActionMailer into test mode
+ActionMailer::Base.delivery_method = :test
+
+# set up carrierwave to use file (instead of say, fog) for testing
+CarrierWave.configure do |config|
+ config.storage = :file
+ config.enable_processing = false
+end
+
+#uncomment the following line to use spork with the debugger
+#require 'spork/ext/ruby-debug'
+
+Spork.prefork do
+ # Loading more in this block will cause your tests to run faster. However,
+ # if you change any configuration or code from libraries loaded here, you'll
+ # need to restart spork for it take effect.
+# This file is copied to spec/ when you run 'rails generate rspec:install'
+ #ENV["RAILS_ENV"] ||= 'test'
+ #require File.expand_path("../../config/environment", __FILE__)
+ require 'rspec/autorun'
+ #require 'rspec/rails'
+# This file was generated by the `rspec --init` command. Conventionally, all
+# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
+# Require this file using `require "spec_helper"` to ensure that it is only
+# loaded once.
+#
+# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+ RSpec.configure do |config|
+ config.treat_symbols_as_metadata_keys_with_true_values = true
+ config.run_all_when_everything_filtered = true
+ config.filter_run :focus
+
+ # you can mark a test as slow so that developers won't commonly hit it, but build server will http://blog.davidchelimsky.net/2010/06/14/filtering-examples-in-rspec-2/
+ config.filter_run_excluding slow: true unless ENV['RUN_SLOW_TESTS'] == "1"
+
+ config.before(:suite) do
+ DatabaseCleaner.strategy = :truncation, {:except => %w[instruments genres] }
+ DatabaseCleaner.clean_with(:truncation, {:except => %w[instruments genres] })
+ end
+
+ config.before(:each) do
+ DatabaseCleaner.start
+ end
+
+ config.after(:each) do
+ DatabaseCleaner.clean
+ end
+
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
+ # examples within a transaction, remove the following line or assign false
+ # instead of true.
+ #config.use_transactional_fixtures = true
+
+ # Run specs in random order to surface order dependencies. If you find an
+ # order dependency and want to debug it, you can fix the order by providing
+ # the seed, which is printed after each run.
+ # --seed 1234
+ config.order = 'random'
+ end
+end
+
+
+Spork.each_run do
+ # This code will be run each time you run your specs.
+end
diff --git a/ruby/spec/uses_temp_files.rb b/ruby/spec/uses_temp_files.rb
new file mode 100644
index 000000000..263bfed1b
--- /dev/null
+++ b/ruby/spec/uses_temp_files.rb
@@ -0,0 +1,33 @@
+#http://gabebw.wordpress.com/2011/03/21/temp-files-in-rspec/
+
+# this will make a folder jam-ruby/spec/tmp if used in an rspec test, and delete it after
+# our .gitignore would also keep spec/tmp out, if somehow it did not get deleted.
+module UsesTempFiles
+ def self.included(example_group)
+ example_group.extend(self)
+ end
+
+ def in_directory_with_file(file)
+ before do
+ @pwd = Dir.pwd
+ @tmp_dir = File.join(File.dirname(__FILE__), 'tmp')
+ FileUtils.mkdir_p(@tmp_dir)
+ Dir.chdir(@tmp_dir)
+
+ FileUtils.mkdir_p(File.dirname(file))
+ FileUtils.touch(file)
+ end
+
+ define_method(:content_for_file) do |content|
+ f = File.new(File.join(@tmp_dir, file), 'a+')
+ f.write(content)
+ f.flush # VERY IMPORTANT
+ f.close
+ end
+
+ after do
+ Dir.chdir(@pwd)
+ FileUtils.rm_rf(@tmp_dir)
+ end
+ end
+end
\ No newline at end of file