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 + + + + + + + + + + +
JamKazam
+ + + + + + + + + + <% unless @suppress_user_has_account_footer == true %> + + + + <% end %> +

<%= yield(:title) %>

+

<%= yield %>

+
+ + + + +

<%= yield(:note) %>

+
+ + +
+ + +
+ + +

+

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

+ +
+ + + + +
Copyright © <%= Time.now.year %> JamKazam, Inc. All rights reserved. +
+ + + diff --git a/ruby/lib/jam_ruby/app/views/layouts/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 + + + + + + + + + + +
JamKazam
+ + + + + + + + + <% unless @suppress_user_has_account_footer == true %> + + + + + <% end %> +

<%= yield(:title) %>

+

<%= yield %>

+
+
+ + +
+ + +

+

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

+ +
+ + + + +
Copyright © <%= Time.now.year %> JamKazam, Inc. All rights reserved. +
+ + + diff --git a/ruby/lib/jam_ruby/app/views/layouts/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