From 614cfcbe853ec3dc611950981cf03f5be8c5189b Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Mon, 6 Jul 2015 15:34:27 -0500 Subject: [PATCH] Merge feature/calendaring branch: commit 8023d6481cbadd52e58b9a4342ac7636ce1807e3 VRFS-3276 : Hook calendar creation into user controller API. Add test to verify. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 3a35002a46f870e2c490b88b3187e0b1569494fd VRFS-3276 : Calendar cleanup job * Add cleanup method to calendar manager * Create a daily job. * Add calendar cleanup to that job. * Add CRON entry * Daily job/ calendar cleanup test cases * Fix calendar manager spec for new required attribute commit 3ff5910f1f019ae8bcb5afe72a31f1d38bb7d7a3 VRFS-3276 : Add a delete-calendar directive when RSVP is canceled. VRFS-3276 : Include path to partial. This fails depending on the method used to start the web server. commit d2441cbf57e50895ac3b40534873c5d529cb3c4f VRFS-3276 : Test new calendar features. Use icalendar gem in test mode only to more deeply verify calendar in strict mode. commit 9ac272a0fb1e58d8cf9f02e7a0e04caada41f659 VRFS-3276 : Calendar manager updates to include manual calendars. Some refactoring to keep common stuff in one place. commit b5d0c758f0dcae41a5f24635e9da9ce6eda56670 VRFS-3276 : Schema, model updates and new calendar model. commit 20472b6b26c88c04edb9bc698e0c06c549e12eb5 VRFS-3276 : Change initial submit behavior of RSVP dialog to display calendar info. The user can then close the dialog after this prompt. commit 77c99103d0221f20ea342169821b90fa987ecf93 VRFS-3276 : Calendar feed markup and styling. Included as partial. commit e632f48600ae23b5f742773310b2a4ac16ae4ee8 VRFS-3276 : Routes and controller implementation of user calendar ICS feed, which uses calendar manager. commit 21fd80a188eae771a65333566b804ade795a1e8c VRFS-3276 : Initial tests for calendar manager commit 92a2524c65abf7b540f9d50049a1b760a5a9927f VRFS-3276 : Calendar manager * Streamline logic * Enable recurring sessions through rrule * Implement method to create ics feed for user * Extract a type-safe scheduled duration method on music_session for external and internal use. commit b71ad3a4cdd943eb84748abaa85fec263b9af468 VRFS-3276 : Include calendar manager commit f8eaafd03647613dafec9f9422282f8613d08e9a VRFS-3276 : Calendar Manager - initial checkin * Create ICS events given individual parameters * Create calendar from music session * Also will create ICS “delete” events --- db/Gemfile.lock | 3 - db/manifest | 1 + db/up/calendar.sql | 13 +++ ruby/Gemfile | 1 + ruby/jt_metadata.json | 2 +- ruby/lib/jam_ruby.rb | 3 + ruby/lib/jam_ruby/calendar_manager.rb | 106 ++++++++++++++++++ ruby/lib/jam_ruby/models/calendar.rb | 14 +++ ruby/lib/jam_ruby/models/music_session.rb | 27 +++-- ruby/lib/jam_ruby/models/rsvp_request.rb | 10 ++ ruby/lib/jam_ruby/models/user.rb | 17 +++ .../jam_ruby/resque/scheduled/daily_job.rb | 17 +++ ruby/spec/jam_ruby/calendar_manager_spec.rb | 85 ++++++++++++++ .../spec/jam_ruby/models/rsvp_request_spec.rb | 14 ++- .../resque/scheduled_daily_job_spec.rb | 43 +++++++ .../javascripts/dialog/rsvpSubmitDialog.js | 8 +- .../stylesheets/client/account.css.scss | 10 ++ .../stylesheets/dialogs/rsvpDialog.css.scss | 27 +++++ web/app/controllers/api_users_controller.rb | 77 +++++++------ .../views/clients/_account_sessions.html.haml | 4 + web/app/views/clients/_calendar.html.slim | 9 ++ .../views/dialogs/_rsvpSubmitDialog.html.haml | 29 +++-- web/config/routes.rb | 2 +- web/config/scheduler.yml | 5 + .../controllers/api_users_controller_spec.rb | 27 ++++- 25 files changed, 485 insertions(+), 69 deletions(-) create mode 100644 db/up/calendar.sql create mode 100644 ruby/lib/jam_ruby/calendar_manager.rb create mode 100644 ruby/lib/jam_ruby/models/calendar.rb create mode 100644 ruby/lib/jam_ruby/resque/scheduled/daily_job.rb create mode 100644 ruby/spec/jam_ruby/calendar_manager_spec.rb create mode 100644 ruby/spec/jam_ruby/resque/scheduled_daily_job_spec.rb create mode 100644 web/app/views/clients/_calendar.html.slim 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'