diff --git a/db/Gemfile.lock b/db/Gemfile.lock index 8d6d039c2..eb6aee107 100644 --- a/db/Gemfile.lock +++ b/db/Gemfile.lock @@ -16,6 +16,3 @@ PLATFORMS DEPENDENCIES pg_migrate (= 0.1.13) - -BUNDLED WITH - 1.10.3 diff --git a/db/manifest b/db/manifest index a3bddb189..8c20bb7ce 100755 --- a/db/manifest +++ b/db/manifest @@ -295,3 +295,4 @@ affiliate_partners2.sql enhance_band_profile.sql broadcast_notifications.sql broadcast_notifications_fk.sql +calendar.sql \ No newline at end of file diff --git a/db/up/calendar.sql b/db/up/calendar.sql new file mode 100644 index 000000000..5e27c7f14 --- /dev/null +++ b/db/up/calendar.sql @@ -0,0 +1,13 @@ +CREATE TABLE calendars ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + target_uid VARCHAR(64) NOT NULL, + name VARCHAR(128), + description VARCHAR(8000), + trigger_delete BOOLEAN DEFAULT FALSE, + start_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + end_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + recurring_mode VARCHAR(50) NOT NULL DEFAULT 'once', + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL +); \ No newline at end of file diff --git a/ruby/Gemfile b/ruby/Gemfile index d46e44e82..0fa0fcb34 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -64,6 +64,7 @@ group :test do gem 'rspec-prof' gem 'time_difference' gem 'byebug' + gem 'icalendar' end # Specify your gem's dependencies in jam_ruby.gemspec diff --git a/ruby/jt_metadata.json b/ruby/jt_metadata.json index cc85875b4..fdcf32faf 100644 --- a/ruby/jt_metadata.json +++ b/ruby/jt_metadata.json @@ -1 +1 @@ -{"container_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150519-97259-1h1tbhj/jam-track-35.jkz", "version": "0", "coverart": null, "rsa_priv_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150519-97259-1h1tbhj/skey.pem", "tracks": [{"name": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150519-97259-1h1tbhj/7452fa4a-0c55-4cb2-948e-221475d7299c.ogg", "trackName": "track_00"}], "rsa_pub_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150519-97259-1h1tbhj/pkey.pem", "jamktrack_info": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/tmpGdncJS"} \ No newline at end of file +{"container_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150706-18103-9lb217/jam-track-45.jkz", "version": "0", "coverart": null, "rsa_priv_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150706-18103-9lb217/skey.pem", "tracks": [{"name": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150706-18103-9lb217/4630741c-69a1-4bc6-8a9f-ec70cb5cd401.ogg", "trackName": "track_00"}], "rsa_pub_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150706-18103-9lb217/pkey.pem", "jamktrack_info": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/tmpmwZtC7"} \ No newline at end of file diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index f132f7b10..969f026ef 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -51,6 +51,7 @@ require "jam_ruby/resque/scheduled/icecast_source_check" require "jam_ruby/resque/scheduled/cleanup_facebook_signup" require "jam_ruby/resque/scheduled/unused_music_notation_cleaner" require "jam_ruby/resque/scheduled/user_progress_emailer" +require "jam_ruby/resque/scheduled/daily_job" require "jam_ruby/resque/scheduled/daily_session_emailer" require "jam_ruby/resque/scheduled/new_musician_emailer" require "jam_ruby/resque/scheduled/music_session_scheduler" @@ -94,6 +95,7 @@ require "jam_ruby/amqp/amqp_connection_manager" require "jam_ruby/database" require "jam_ruby/message_factory" require "jam_ruby/models/backing_track" +require "jam_ruby/models/calendar" require "jam_ruby/models/feedback" require "jam_ruby/models/feedback_observer" #require "jam_ruby/models/max_mind_geo" @@ -227,6 +229,7 @@ require "jam_ruby/models/sale_line_item" require "jam_ruby/models/recurly_transaction_web_hook" require "jam_ruby/models/broadcast_notification" require "jam_ruby/models/broadcast_notification_view" +require "jam_ruby/calendar_manager" require "jam_ruby/jam_tracks_manager" require "jam_ruby/jam_track_importer" require "jam_ruby/jmep_manager" diff --git a/ruby/lib/jam_ruby/calendar_manager.rb b/ruby/lib/jam_ruby/calendar_manager.rb new file mode 100644 index 000000000..523e683d9 --- /dev/null +++ b/ruby/lib/jam_ruby/calendar_manager.rb @@ -0,0 +1,106 @@ +module JamRuby + class CalendarManager < BaseManager + DATE_FORMAT="%Y%m%dT%H%M%SZ" + def initialize(options={}) + super(options) + @log = Logging.logger[self] + end + + def cancel_ics_event(music_session, user) + Calendar.where( + user_id: user.id, + target_uid: music_session.id, + name: music_session.description) + .first_or_create( + description: music_session.description, + start_at: music_session.scheduled_start, + end_at: music_session.scheduled_start+music_session.safe_scheduled_duration, + trigger_delete: true) + + end + + # Remove all "delete" event calendar records older than 4 weeks: + def cleanup() + Calendar.where("trigger_delete=TRUE AND created_at < ?", 4.weeks.ago) + .destroy_all() + end + + # @return event (as ICS string) for a given music session + def ics_event_from_music_session(music_session, delete=false) + # Determine properties of calendar event and create: + uid = "#{music_session.id}@JamKazam" + text = "JamKazam Session #{music_session.description}" + rrule = nil + start_at = music_session.scheduled_start + stop_at = music_session.scheduled_start+music_session.safe_scheduled_duration + if !delete && music_session.recurring_mode==MusicSession::RECURRING_WEEKLY + rrule = "FREQ=WEEKLY;INTERVAL=1" + end + create_ics_event(uid, text, text, start_at, stop_at, delete, rrule) + end + + # @return event (as ICS string) for a given music session + def ics_event_from_calendar(calendar) + # Determine properties of calendar event and create: + rrule = nil + if !calendar.trigger_delete && calendar.recurring_mode==MusicSession::RECURRING_WEEKLY + rrule = "FREQ=WEEKLY;INTERVAL=1" + end + + create_ics_event( + calendar.target_uid, + "JamKazam Session #{calendar.name}", + calendar.description, + calendar.start_at, + calendar.end_at, + calendar.trigger_delete, + rrule + ) + end + + # @return calendar (as ICS string) for specified user + # Includes all RSVPed sessions, as well as any calendar + # entries for the given user: + def create_ics_feed(user) + ics_events = "" + MusicSession.scheduled_rsvp(user, true).each do |music_session| + ics_events << "\r\n" if(ics_events.length != 0) + ics_events << ics_event_from_music_session(music_session) + end + + user.calendars.each do |user| + ics_events << "\r\n" if(ics_events.length != 0) + ics_events << ics_event_from_calendar(user) + end + + create_ics_cal(ics_events) + end + + # @return event (as ICS string) for given arguments + def create_ics_event(uuid, name, description, start_at, end_at, delete=false, rrule=nil, sequence=nil) + uuid ||= UUID.timestamp_create + event = "BEGIN:VEVENT\r\n" + event << "UID:#{uuid}\r\n" + event << "DTSTAMP:#{Time.now.utc().strftime(DATE_FORMAT)}\r\n" + event << "DTSTART:#{start_at.utc().strftime(DATE_FORMAT)}\r\n" + event << "DTEND:#{end_at.utc().strftime(DATE_FORMAT)}\r\n" + event << "SUMMARY:#{name}\r\n" + event << "DESCRIPTION:#{description}\r\n" + if delete + event << "METHOD:CANCEL\r\n" + event << "STATUS:CANCELLED\r\n" + end + if rrule + event << "RRULE:#{rrule}\r\n" + end + event << "SEQUENCE:#{sequence}\r\n" if sequence + event << "END:VEVENT" + end + + # @return calendar (as ICS string) for specified events + def create_ics_cal(ics_events) + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:JamKazam\r\n#{ics_events}\r\nEND:VCALENDAR" + end + + end # class +end # module \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/calendar.rb b/ruby/lib/jam_ruby/models/calendar.rb new file mode 100644 index 000000000..4f244b587 --- /dev/null +++ b/ruby/lib/jam_ruby/models/calendar.rb @@ -0,0 +1,14 @@ +module JamRuby + class Calendar < ActiveRecord::Base + include HtmlSanitize + html_sanitize strict: [:name, :description] + attr_accessible :name, :description, :target_uid, :trigger_delete, :start_at, :end_at + + @@log = Logging.logger[Calendar] + + self.table_name = "calendars" + self.primary_key = 'id' + + belongs_to :user, :class_name => 'JamRuby::User', :foreign_key => :user_id, :inverse_of => :calendars + end +end diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index 96b9b7831..94b2295d4 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -880,6 +880,21 @@ SQL end result end + + def safe_scheduled_duration + duration = scheduled_duration + # you can put seconds into the scheduled_duration field, but once stored, it comes back out as a string + if scheduled_duration.class == String + begin + bits = scheduled_duration.split(':') + duration = bits[0].to_i.hours + bits[1].to_i.minutes + bits[2].to_i.seconds + rescue Exception => e + duration = 1.hours + @@log.error("unable to parse duration #{scheduled_duration}") + end + end + duration + end # should create a timestamp like: # # with_timezone = TRUE @@ -910,17 +925,7 @@ SQL end end - duration = scheduled_duration - # you can put seconds into the scheduled_duration field, but once stored, it comes back out as a string - if scheduled_duration.class == String - begin - bits = scheduled_duration.split(':') - duration = bits[0].to_i.hours + bits[1].to_i.minutes + bits[2].to_i.seconds - rescue Exception => e - duration = 1.hours - @@log.error("unable to parse duration #{scheduled_duration}") - end - end + duration = safe_scheduled_duration end_time = start_time + duration if with_timezone "#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} #{timezone_display}" diff --git a/ruby/lib/jam_ruby/models/rsvp_request.rb b/ruby/lib/jam_ruby/models/rsvp_request.rb index f416640bf..20c3b9e9a 100644 --- a/ruby/lib/jam_ruby/models/rsvp_request.rb +++ b/ruby/lib/jam_ruby/models/rsvp_request.rb @@ -8,6 +8,7 @@ module JamRuby validates :user, presence: true validates :canceled, :inclusion => {:in => [nil, true, false]} validate :creator_rsvp_cancel + before_save :cancel_calendar # pulls all instruments from the associated rsvp_slots def instrument_list @@ -305,6 +306,15 @@ module JamRuby errors.add(:canceled, "can't be canceled by the session organizer") end end + + def cancel_calendar + calendar_manager = CalendarManager.new + if self.canceled + self.rsvp_slots.each do |slot| + calendar_manager.cancel_ics_event(slot.music_session, user) + end + end + end end end diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index ecd0a3501..0500a896b 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -45,6 +45,9 @@ module JamRuby # authorizations (for facebook, etc -- omniauth) has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization" + # calendars (for scheduling NOT in music_session) + has_many :calendars, :class_name => "JamRuby::Calendar" + # connections (websocket-gateway) has_many :connections, :class_name => "JamRuby::Connection" @@ -698,6 +701,20 @@ module JamRuby end end + # Build calendars using given parameter. + # @param calendars (array of hash) + def update_calendars(calendars) + unless self.new_record? + Calendar.where("user_id = ?", self.id).delete_all + end + + unless calendars.nil? + calendars.each do |cal| + self.calendars << self.calendars.create(cal) + end + end + end + # given an array of instruments, update a user's instruments def update_instruments(instruments) # delete all instruments for this user first diff --git a/ruby/lib/jam_ruby/resque/scheduled/daily_job.rb b/ruby/lib/jam_ruby/resque/scheduled/daily_job.rb new file mode 100644 index 000000000..388516441 --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/daily_job.rb @@ -0,0 +1,17 @@ +module JamRuby + class DailyJob + extend Resque::Plugins::JamLonelyJob + + @queue = :scheduled_daily_job + @@log = Logging.logger[DailyJob] + + class << self + def perform + @@log.debug("waking up") + calendar_manager = CalendarManager.new + calendar_manager.cleanup() + @@log.debug("done") + end + end + end +end diff --git a/ruby/spec/jam_ruby/calendar_manager_spec.rb b/ruby/spec/jam_ruby/calendar_manager_spec.rb new file mode 100644 index 000000000..1532fca64 --- /dev/null +++ b/ruby/spec/jam_ruby/calendar_manager_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' +require 'icalendar' + +describe CalendarManager do + CALENDAR_NAME="Test Cal" + + before :all do + @genre1 = FactoryGirl.create(:genre) + @calendar_manager = JamRuby::CalendarManager.new + + # Time resolution is seconds: + @start = Time.at(Time.now.utc.to_i) + @stop =(@start+1.hours) + end + + before(:each) do + + end + + describe "with music sessions" do + before :all do + @creator = FactoryGirl.create(:user) + @music_session = FactoryGirl.create(:music_session, :creator => @creator, :description => CALENDAR_NAME, :genre => @genre1, :scheduled_start=>@start, :scheduled_duration=>3600) + @music_session.reload + end + + it "validator detects bad calendar" do + lambda{verify_ical("Bad medicine calendar")} + .should raise_error(RuntimeError) + end + + it "can create calendar feed" do + ics = @calendar_manager.create_ics_feed(@creator) + # Basic format checking...there are some online tools that + # check a lot more, but no ruby libs that I could find: + lines = ics.split("\r\n") + lines.should have(12).items + lines.first.should eq("BEGIN:VCALENDAR") + lines.last.should eq("END:VCALENDAR") + lines[-2].should eq("END:VEVENT") + verify_ical(ics) + end + end + + describe "with manual calendars" do + before :all do + @creator = FactoryGirl.create(:user) + @creator.calendars<CALENDAR_NAME, :description=>"This is a test", :start_at=>(@start), :end_at=>@stop, :trigger_delete=>false, :target_uid=>"2112"}) + end + + it "can create calendar feed" do + #pending "foobar" + ics = @calendar_manager.create_ics_feed(@creator) + + # Basic format checking...there are some online tools that + # check a lot more, but no ruby libs that I could find: + lines = ics.split("\r\n") + lines.should have(12).items + lines.first.should eq("BEGIN:VCALENDAR") + lines.last.should eq("END:VCALENDAR") + lines[-2].should eq("END:VEVENT") + verify_ical(ics) + end + end + + def verify_ical(ics) + strict_parser = Icalendar::Parser.new(ics, true) + cals = strict_parser.parse + cals.should_not be_nil + cals.should have(1).items + + cal = cals.first + cal.should_not be_nil + cal.events.should have(1).items + event = cal.events.first + event.should_not be_nil + + event.summary.should eq("JamKazam Session #{CALENDAR_NAME}") + event.dtstart.to_i.should_not be_nil + event.dtend.to_i.should_not be_nil + (event.dtstart).to_time.utc.to_i.should eq(@start.to_i) + (event.dtend).to_time.utc.to_i.should eq(@stop.to_i) + end +end + diff --git a/ruby/spec/jam_ruby/models/rsvp_request_spec.rb b/ruby/spec/jam_ruby/models/rsvp_request_spec.rb index f93501647..9da353746 100644 --- a/ruby/spec/jam_ruby/models/rsvp_request_spec.rb +++ b/ruby/spec/jam_ruby/models/rsvp_request_spec.rb @@ -30,10 +30,10 @@ describe RsvpRequest do @slot1 = FactoryGirl.build(:rsvp_slot, :music_session => @music_session, :instrument => JamRuby::Instrument.find('electric guitar')) @slot1.save - + @slot2 = FactoryGirl.build(:rsvp_slot, :music_session => @music_session, :instrument => JamRuby::Instrument.find('drums')) @slot2.save - + @invitation = FactoryGirl.build(:invitation, :sender => @session_creator, :receiver => @session_invitee, :music_session => @music_session) @invitation.save end @@ -53,12 +53,12 @@ describe RsvpRequest do @music_session.save RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id]}, @non_session_invitee) - expect {RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id]}, @non_session_invitee)}.to raise_error(JamRuby::StateError) + expect {RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id]}, @non_session_invitee)}.to raise_error(JamRuby::StateError) end it "should allow invitee to RSVP to session with closed RSVPs" do rsvp = RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id], :message => "We be jammin!"}, @session_invitee) - + # verify comment comment = SessionInfoComment.find_by_creator_id(@session_invitee) comment.comment.should == "We be jammin!" @@ -373,12 +373,14 @@ describe RsvpRequest do comment = SessionInfoComment.find_by_creator_id(@session_invitee) comment.comment.should == "Let's Jam!" - # cancel - expect {RsvpRequest.cancel({:id => rsvp.id, :session_id => @music_session.id, :cancelled => "all", :message => "Sorry, I'm bailing for all sessions"}, @session_invitee)}.to_not raise_error + calendar_count = Calendar.find(:all).count + # cancel & check that calendar has been added: + expect {RsvpRequest.cancel({:id => rsvp.id, :session_id => @music_session.id, :cancelled => "all", :message => "Sorry, I'm bailing for all sessions"}, @session_invitee)}.to_not raise_error rsvp = RsvpRequest.find_by_id(rsvp.id) rsvp.canceled.should == true rsvp.cancel_all.should == true + (Calendar.find(:all).count - calendar_count).should eq(1) # verify comment comment = SessionInfoComment.find_by_creator_id(@session_invitee) diff --git a/ruby/spec/jam_ruby/resque/scheduled_daily_job_spec.rb b/ruby/spec/jam_ruby/resque/scheduled_daily_job_spec.rb new file mode 100644 index 000000000..b53fdbca5 --- /dev/null +++ b/ruby/spec/jam_ruby/resque/scheduled_daily_job_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe 'DailyJob' do + describe "calendar cleanup" do + shared_examples_for :calendar_cleanup do |trigger_delete, end_count| + before :each do + Calendar.destroy_all + @creator = FactoryGirl.create(:user) + @creator.calendars << Calendar.new( + :name=>"Test Cal", + :description=>"This is a test", + :start_at=>(Time.now), + :end_at=>Time.now, + :trigger_delete=>trigger_delete, + :target_uid=>"2112" + ) + end + + it "properly purges old 'delete' calendars" do + @creator.reload + @creator.calendars.should have(1).items + + JamRuby::DailyJob.perform + @creator.reload + @creator.calendars.should have(1).items + + Timecop.travel(Time.now + 5.weeks) + JamRuby::DailyJob.perform + @creator.reload + @creator.calendars.should have(end_count).items + Timecop.return + end + end + + describe "whacks old 'delete' calendars" do + it_behaves_like :calendar_cleanup, true, 0 + end + + describe "doesn't whacks non 'delete' calendars" do + it_behaves_like :calendar_cleanup, false, 1 + end + end # calendar cleanpu +end #spec diff --git a/web/app/assets/javascripts/dialog/rsvpSubmitDialog.js b/web/app/assets/javascripts/dialog/rsvpSubmitDialog.js index 9b29fcb28..d81b1de38 100644 --- a/web/app/assets/javascripts/dialog/rsvpSubmitDialog.js +++ b/web/app/assets/javascripts/dialog/rsvpSubmitDialog.js @@ -10,6 +10,7 @@ var dialogId = 'rsvp-submit-dialog'; var $btnSubmit = $("#btnSubmitRsvp"); + function beforeShow(data) { $('.error', $dialog).hide(); } @@ -56,7 +57,6 @@ $btnSubmit.unbind('click'); $btnSubmit.click(function(e) { e.preventDefault(); - var error = false; var slotIds = []; var selectedSlots = []; @@ -96,7 +96,11 @@ if (!error) { $dialog.triggerHandler(EVENTS.RSVP_SUBMITTED); - app.layout.closeDialog(dialogId); + + // Show confirmation & calendar; hide regular buttons. + $(".rsvp-options").addClass("hidden") + $(".rsvp-confirm").removeClass("hidden") + $(".buttons").addClass("hidden") } }) .fail(function(xhr, textStatus, errorMessage) { diff --git a/web/app/assets/stylesheets/client/account.css.scss b/web/app/assets/stylesheets/client/account.css.scss index fae2ccd46..606bd73f8 100644 --- a/web/app/assets/stylesheets/client/account.css.scss +++ b/web/app/assets/stylesheets/client/account.css.scss @@ -4,6 +4,16 @@ .session-detail-scroller, #account-identity-content-scroller { + .ics-feed-caption { + font-size: 1.2em; + margin: 0em 0em 1em 0em; + } + + .ics-feed-link { + font-size: 1.1em; + margin: 0.5em 0em 1em 0em; + } + .content-wrapper { padding:10px 30px; } diff --git a/web/app/assets/stylesheets/dialogs/rsvpDialog.css.scss b/web/app/assets/stylesheets/dialogs/rsvpDialog.css.scss index fc5c4879c..ff7c66b83 100644 --- a/web/app/assets/stylesheets/dialogs/rsvpDialog.css.scss +++ b/web/app/assets/stylesheets/dialogs/rsvpDialog.css.scss @@ -3,6 +3,33 @@ .rsvp-dialog { min-height:initial; + height:auto; + + .rsvp-confirm { + color: white; + margin-top: 1em; + .ics-feed-caption { + font-size: 1.2em; + margin: 0em 0em 1em 0em; + } + + .ics-feed-link { + font-size: 1.1em; + margin: 0.5em 0em 1em 0em; + } + + .ics-help-link { + display: inline; + font-size: 0.8em; + padding-right: 2em; + } + + .confirm-buttons { + text-align: center; + margin: 1em 0em 0em 0em; + } + } + .session-name { margin:3px 0 0; diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index ebe68290a..5c8f27377 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -1,7 +1,7 @@ require 'sanitize' class ApiUsersController < ApiController - before_filter :api_signed_in_user, :except => [:create, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump, :validate_data] + before_filter :api_signed_in_user, :except => [:create, :calendar, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump, :validate_data] before_filter :auth_user, :only => [:session_settings_show, :session_history_index, :session_user_history_index, :update, :delete, :liking_create, :liking_destroy, # likes :following_create, :following_show, :following_destroy, # followings @@ -15,19 +15,22 @@ class ApiUsersController < ApiController :share_session, :share_recording, :affiliate_report, :audio_latency, :broadcast_notification] - respond_to :json + respond_to :json, :except => :calendar + respond_to :ics, :only => :calendar def index @users = User.paginate(page: params[:page]) respond_with @users, responder: ApiResponder, :status => 200 end + def calendar + @user=lookup_user + ics = CalendarManager.new.create_ics_feed(@user) + send_data ics, :filename => 'JamKazam', :disposition => 'inline', :type => "text/calendar" + end + def show - @user = User.includes([{musician_instruments: :instrument}, - {band_musicians: :user}, - {genre_players: :genre}, - :bands, :instruments, :genres, :jam_track_rights, :affiliate_partner]) - .find(params[:id]) + @user=lookup_user respond_with @user, responder: ApiResponder, :status => 200 end @@ -80,10 +83,10 @@ class ApiUsersController < ApiController respond_with_model(@user, new: true, location: lambda { return api_user_detail_url(@user.id) }) end end - + def profile_save end - + def update @user = User.find(params[:id]) @@ -96,7 +99,7 @@ class ApiUsersController < ApiController @user.country = params[:country] if params.has_key?(:country) @user.musician = params[:musician] if params.has_key?(:musician) @user.update_instruments(params[:instruments].nil? ? [] : params[:instruments]) if params.has_key?(:instruments) - + # genres @user.update_genres(params[:genres].nil? ? [] : params[:genres], GenrePlayer::PROFILE) if params.has_key?(:genres) @user.update_genres(params[:virtual_band_genres].nil? ? [] : params[:virtual_band_genres], GenrePlayer::VIRTUAL_BAND) if params.has_key?(:virtual_band_genres) @@ -104,7 +107,7 @@ class ApiUsersController < ApiController @user.update_genres(params[:paid_session_genres].nil? ? [] : params[:paid_session_genres], GenrePlayer::PAID_SESSION) if params.has_key?(:paid_session_genres) @user.update_genres(params[:free_session_genres].nil? ? [] : params[:free_session_genres], GenrePlayer::FREE_SESSION) if params.has_key?(:free_session_genres) @user.update_genres(params[:cowriting_genres].nil? ? [] : params[:cowriting_genres], GenrePlayer::COWRITING) if params.has_key?(:cowriting_genres) - + @user.show_whats_next = params[:show_whats_next] if params.has_key?(:show_whats_next) @user.show_whats_next_count = params[:show_whats_next_count] if params.has_key?(:show_whats_next_count) @user.subscribe_email = params[:subscribe_email] if params.has_key?(:subscribe_email) @@ -146,7 +149,7 @@ class ApiUsersController < ApiController @user.update_online_presences(params[:online_presences]) if params.has_key?(:online_presences) @user.update_performance_samples(params[:performance_samples]) if params.has_key?(:performance_samples) - + @user.update_calendars(params[:calendars]) if params.has_key?(:calendars) @user.save if @user.errors.any? @@ -196,9 +199,9 @@ class ApiUsersController < ApiController end def delete - @user.destroy + @user.destroy respond_with responder: ApiResponder, :status => 204 - end + end def signup_confirm @user = UserManager.new.signup_confirm(params[:signup_token]) @@ -260,7 +263,7 @@ class ApiUsersController < ApiController def auth_session_delete sign_out render :json => { :success => true }, :status => 200 - end + end ###################### SESSION SETTINGS ################### def session_settings_show @@ -276,7 +279,7 @@ class ApiUsersController < ApiController @session_user_history = @user.session_user_history(params[:id], params[:session_id]) end - ###################### BANDS ######################## + ###################### BANDS ######################## def band_index @bands = User.band_index(params[:id]) end @@ -296,7 +299,7 @@ class ApiUsersController < ApiController @user = User.find(params[:id]) if !params[:user_id].nil? @user.create_user_liking(params[:user_id]) - + elsif !params[:band_id].nil? @user.create_band_liking(params[:band_id]) end @@ -454,7 +457,7 @@ class ApiUsersController < ApiController respond_with @invitation, responder: ApiResponder, :status => 200 rescue ActiveRecord::RecordNotFound - render :json => { :message => ValidationMessages::BAND_INVITATION_NOT_FOUND }, :status => 404 + render :json => { :message => ValidationMessages::BAND_INVITATION_NOT_FOUND }, :status => 404 end end @@ -467,9 +470,9 @@ class ApiUsersController < ApiController params[:accepted]) respond_with @invitation, responder: ApiResponder, :status => 200 - + rescue ActiveRecord::RecordNotFound - render :json => { :message => ValidationMessages::BAND_INVITATION_NOT_FOUND }, :status => 404 + render :json => { :message => ValidationMessages::BAND_INVITATION_NOT_FOUND }, :status => 404 end end @@ -576,11 +579,11 @@ class ApiUsersController < ApiController # user_id is deduced if possible from the user's cookie. @dump = CrashDump.new - @dump.client_type = params[:client_type] + @dump.client_type = params[:client_type] @dump.client_version = params[:client_version] @dump.client_id = params[:client_id] @dump.user_id = current_user.try(:id) - @dump.session_id = params[:session_id] + @dump.session_id = params[:session_id] @dump.timestamp = params[:timestamp] unless @dump.save @@ -589,7 +592,7 @@ class ApiUsersController < ApiController respond_with @dump return end - + # This part is the piece that really needs to be decomposed into a library... if Rails.application.config.storage_type == :fog s3 = AWS::S3.new(:access_key_id => Rails.application.config.aws_access_key_id, @@ -597,15 +600,15 @@ class ApiUsersController < ApiController bucket = s3.buckets[Rails.application.config.aws_bucket] uri = @dump.uri expire = Time.now + 20.years - read_url = bucket.objects[uri].url_for(:read, - :expires => expire, + read_url = bucket.objects[uri].url_for(:read, + :expires => expire, :'response_content_type' => 'application/octet-stream').to_s @dump.update_attribute(:uri, read_url) - write_url = bucket.objects[uri].url_for(:write, - :expires => Rails.application.config.crash_dump_data_signed_url_timeout, + write_url = bucket.objects[uri].url_for(:write, + :expires => Rails.application.config.crash_dump_data_signed_url_timeout, :'response_content_type' => 'application/octet-stream').to_s - + logger.debug("crash_dump can read from url #{read_url}") redirect_to write_url @@ -744,9 +747,9 @@ class ApiUsersController < ApiController if txt = oo.affiliate_legalese.try(:legalese) txt = ControllerHelp.instance.simple_format(txt) end - result['agreement'] = { - 'legalese' => txt, - 'signed_at' => oo.signed_at + result['agreement'] = { + 'legalese' => txt, + 'signed_at' => oo.signed_at } #result['signups'] = oo.referrals_by_date #result['earnings'] = [['April 2015', '1000 units', '$100']] @@ -851,7 +854,7 @@ class ApiUsersController < ApiController else render json: { message: 'Valid Site', data: data }, status: 200 end - else + else render json: { message: "unknown validation for data '#{params[:data]}', site '#{params[:site]}'" }, status: :unprocessable_entity end end @@ -880,6 +883,14 @@ class ApiUsersController < ApiController render json: { }, status: 200 end + def lookup_user + User.includes([{musician_instruments: :instrument}, + {band_musicians: :user}, + {genre_players: :genre}, + :bands, :instruments, :genres, :jam_track_rights, :affiliate_partner]) + .find(params[:id]) + end + ###################### RECORDINGS ####################### # def recording_index # @recordings = User.recording_index(current_user, params[:id]) @@ -932,5 +943,5 @@ class ApiUsersController < ApiController # @recording = Recording.find(params[:recording_id]) # @recording.delete # respond_with responder: ApiResponder, :status => 204 - # end + # end end diff --git a/web/app/views/clients/_account_sessions.html.haml b/web/app/views/clients/_account_sessions.html.haml index 8e0824c28..6489fb7e1 100644 --- a/web/app/views/clients/_account_sessions.html.haml +++ b/web/app/views/clients/_account_sessions.html.haml @@ -22,6 +22,10 @@ %thead %tbody .clearall + .content-wrapper + .ics-feed-caption Following is a URL for your personal JamKazam .ics calendar, which tracks all sessions and events to which you have RSVP'd: + =render "calendar" + / end content scrolling area %script{type: 'text/template', id: 'template-account-session'} diff --git a/web/app/views/clients/_calendar.html.slim b/web/app/views/clients/_calendar.html.slim new file mode 100644 index 000000000..85c808d2f --- /dev/null +++ b/web/app/views/clients/_calendar.html.slim @@ -0,0 +1,9 @@ +-if current_user + .account-calendar + .ics-feed-link + =api_users_calendar_feed_url(current_user) + .ics-help-links + .ics-help-link + a href="" How to subscribe to your calendar in Google Calendar + .ics-help-link + a href="" How to subscribe to your calendar in Microsoft Outlook diff --git a/web/app/views/dialogs/_rsvpSubmitDialog.html.haml b/web/app/views/dialogs/_rsvpSubmitDialog.html.haml index 1135fde57..f7eb20bbf 100644 --- a/web/app/views/dialogs/_rsvpSubmitDialog.html.haml +++ b/web/app/views/dialogs/_rsvpSubmitDialog.html.haml @@ -7,16 +7,27 @@ .session-name .scheduled-start .schedule-recurrence - .part - .slot-instructions Check the box(es) next to the track(s) you want to play in the session: - .error{:style => 'display:none'} - .rsvp-instruments + .rsvp-options + .part + .slot-instructions Check the box(es) next to the track(s) you want to play in the session: + .error{:style => 'display:none'} + .rsvp-instruments + + .comment-instructions Enter a message to the other musicians in the session (optional): + %textarea.txtComment{rows: '2', placeholder: 'Enter a comment...'} + .rsvp-confirm.hidden + %p SUCCESS! + %br + %p We recommend that you subscribe to your own personal JamKazam calendar in your favorite calendar app to help you remember this session, as well as other sessions and events to which you RSVP. + %br + %p Here is the URL for your calendar: + =render "clients/calendar" + .confirm-buttons + %a#btnClose.button-grey{'layout-action' => 'close'} CLOSE - .comment-instructions Enter a message to the other musicians in the session (optional): - %textarea.txtComment{rows: '2', placeholder: 'Enter a comment...'} .buttons .left - %a.button-grey{:href => 'http://jamkazam.desk.com', :rel => 'external', :target => '_blank'} HELP + %a#btnHelp.button-grey{:href => 'http://jamkazam.desk.com', :rel => 'external', :target => '_blank'} HELP .right - %a.button-grey{:id => 'btnCancel', 'layout-action' => 'close'} CANCEL - %a.button-orange{:id => 'btnSubmitRsvp'} SUBMIT RSVP \ No newline at end of file + %a#btnCancel.button-grey{'layout-action' => 'close'} CANCEL + %a#btnSubmitRsvp.button-orange SUBMIT RSVP \ No newline at end of file diff --git a/web/config/routes.rb b/web/config/routes.rb index 43ee05098..924abf233 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -271,9 +271,9 @@ SampleApp::Application.routes.draw do #match '/users' => 'api_users#create', :via => :post match '/users/:id' => 'api_users#update', :via => :post match '/users/:id' => 'api_users#delete', :via => :delete + match '/users/:id/calendar.ics' => 'api_users#calendar', :via => :get, :as => 'api_users_calendar_feed' match '/users/confirm/:signup_token' => 'api_users#signup_confirm', :via => :post, :as => 'api_signup_confirmation' match '/users/complete/:signup_token' => 'api_users#complete', as: 'complete', via: 'post' - match '/users/:id/set_password' => 'api_users#set_password', :via => :post # recurly diff --git a/web/config/scheduler.yml b/web/config/scheduler.yml index 1890a41d4..40aa901fc 100644 --- a/web/config/scheduler.yml +++ b/web/config/scheduler.yml @@ -40,6 +40,11 @@ DailySessionEmailer: class: "JamRuby::DailySessionEmailer" description: "Sends daily scheduled session emails" +DailyJob: + cron: "0 4 * * *" + class: "JamRuby::DailyJob" + description: "Aggregate task to perform general daily things" + ScheduledMusicSessionCleaner: cron: "0 3 * * *" class: "JamRuby::ScheduledMusicSessionCleaner" diff --git a/web/spec/controllers/api_users_controller_spec.rb b/web/spec/controllers/api_users_controller_spec.rb index a95ffebf7..065602e77 100644 --- a/web/spec/controllers/api_users_controller_spec.rb +++ b/web/spec/controllers/api_users_controller_spec.rb @@ -59,6 +59,27 @@ describe ApiUsersController do end end + describe "calendars" do + before :each do + Calendar.destroy_all + end + + it "adds calendar via update" do + cals = [{ + :name=>"Test Cal", + :description=>"This is a test", + :start_at=>(Time.now), + :end_at=>Time.now, + :trigger_delete=>true, + :target_uid=>"2112" + }] + post :update, id:user.id, calendars: cals, :format=>'json' + response.should be_success + user.reload + user.calendars.should have(1).items + end + end + describe "update mod" do it "empty mod" do post :update, id:user.id, mods: {}, :format=>'json' @@ -83,13 +104,13 @@ describe ApiUsersController do end end - describe 'site validation' do + describe 'site validation' do - it 'checks valid and invalid site types' do + it 'checks valid and invalid site types' do site_types = Utils::SITE_TYPES.clone << 'bandcamp-fan' site_types.each do |sitetype| rec_id = nil - case sitetype + case sitetype when 'url' valid, invalid = 'http://jamkazam.com', 'http://jamkazamxxx.com' when 'youtube'