Compare commits

...

14 Commits

Author SHA1 Message Date
Steven Miers 8023d6481c VRFS-3276 : Hook calendar creation into user controller API. Add test to verify. 2015-07-06 11:26:02 -05:00
Steven Miers 3a35002a46 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
2015-07-03 16:34:40 -05:00
Steven Miers 3ff5910f1f VRFS-3276 : Add a delete-calendar directive when RSVP is canceled. 2015-07-02 11:19:26 -05:00
Steven Miers bd23b28cd0 VRFS-3276 : Include path to partial. This fails depending on the method used to start the web server. 2015-07-01 22:09:34 -05:00
Steven Miers d2441cbf57 VRFS-3276 : Test new calendar features. Use icalendar gem in test mode only to more deeply verify calendar in strict mode. 2015-07-01 20:53:25 -05:00
Steven Miers 9ac272a0fb VRFS-3276 : Calendar manager updates to include manual calendars. Some refactoring to keep common stuff in one place. 2015-07-01 20:52:10 -05:00
Steven Miers b5d0c758f0 VRFS-3276 : Schema, model updates and new calendar model. 2015-07-01 20:51:17 -05:00
Steven Miers 20472b6b26 VRFS-3276 : Change initial submit behavior of RSVP dialog to display calendar info. The user can then close the dialog after this prompt. 2015-06-29 17:36:08 -05:00
Steven Miers 77c99103d0 VRFS-3276 : Calendar feed markup and styling. Included as partial. 2015-06-29 13:12:54 -05:00
Steven Miers e632f48600 VRFS-3276 : Routes and controller implementation of user calendar ICS feed, which uses calendar manager. 2015-06-29 13:11:57 -05:00
Steven Miers 21fd80a188 VRFS-3276 : Initial tests for calendar manager 2015-06-26 14:15:19 -05:00
Steven Miers 92a2524c65 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.
2015-06-26 13:56:56 -05:00
Steven Miers b71ad3a4cd VRFS-3276 : Include calendar manager 2015-06-26 13:54:37 -05:00
Steven Miers f8eaafd036 VRFS-3276 : Calendar Manager - initial checkin
* Create ICS events given individual parameters
* Create calendar from music session
* Also will create ICS “delete” events
2015-06-25 08:51:13 -05:00
25 changed files with 485 additions and 69 deletions

View File

@ -16,6 +16,3 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
pg_migrate (= 0.1.13) pg_migrate (= 0.1.13)
BUNDLED WITH
1.10.3

View File

@ -295,3 +295,4 @@ affiliate_partners2.sql
enhance_band_profile.sql enhance_band_profile.sql
broadcast_notifications.sql broadcast_notifications.sql
broadcast_notifications_fk.sql broadcast_notifications_fk.sql
calendar.sql

13
db/up/calendar.sql Normal file
View File

@ -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
);

View File

@ -64,6 +64,7 @@ group :test do
gem 'rspec-prof' gem 'rspec-prof'
gem 'time_difference' gem 'time_difference'
gem 'byebug' gem 'byebug'
gem 'icalendar'
end end
# Specify your gem's dependencies in jam_ruby.gemspec # Specify your gem's dependencies in jam_ruby.gemspec

View File

@ -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"} {"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"}

View File

@ -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/cleanup_facebook_signup"
require "jam_ruby/resque/scheduled/unused_music_notation_cleaner" require "jam_ruby/resque/scheduled/unused_music_notation_cleaner"
require "jam_ruby/resque/scheduled/user_progress_emailer" 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/daily_session_emailer"
require "jam_ruby/resque/scheduled/new_musician_emailer" require "jam_ruby/resque/scheduled/new_musician_emailer"
require "jam_ruby/resque/scheduled/music_session_scheduler" 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/database"
require "jam_ruby/message_factory" require "jam_ruby/message_factory"
require "jam_ruby/models/backing_track" require "jam_ruby/models/backing_track"
require "jam_ruby/models/calendar"
require "jam_ruby/models/feedback" require "jam_ruby/models/feedback"
require "jam_ruby/models/feedback_observer" require "jam_ruby/models/feedback_observer"
#require "jam_ruby/models/max_mind_geo" #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/recurly_transaction_web_hook"
require "jam_ruby/models/broadcast_notification" require "jam_ruby/models/broadcast_notification"
require "jam_ruby/models/broadcast_notification_view" require "jam_ruby/models/broadcast_notification_view"
require "jam_ruby/calendar_manager"
require "jam_ruby/jam_tracks_manager" require "jam_ruby/jam_tracks_manager"
require "jam_ruby/jam_track_importer" require "jam_ruby/jam_track_importer"
require "jam_ruby/jmep_manager" require "jam_ruby/jmep_manager"

View File

@ -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

View File

@ -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

View File

@ -880,6 +880,21 @@ SQL
end end
result result
end 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: # should create a timestamp like:
# #
# with_timezone = TRUE # with_timezone = TRUE
@ -910,17 +925,7 @@ SQL
end end
end end
duration = scheduled_duration duration = safe_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
end_time = start_time + duration end_time = start_time + duration
if with_timezone if with_timezone
"#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} #{timezone_display}" "#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} #{timezone_display}"

View File

@ -8,6 +8,7 @@ module JamRuby
validates :user, presence: true validates :user, presence: true
validates :canceled, :inclusion => {:in => [nil, true, false]} validates :canceled, :inclusion => {:in => [nil, true, false]}
validate :creator_rsvp_cancel validate :creator_rsvp_cancel
before_save :cancel_calendar
# pulls all instruments from the associated rsvp_slots # pulls all instruments from the associated rsvp_slots
def instrument_list def instrument_list
@ -305,6 +306,15 @@ module JamRuby
errors.add(:canceled, "can't be canceled by the session organizer") errors.add(:canceled, "can't be canceled by the session organizer")
end end
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
end end

View File

@ -45,6 +45,9 @@ module JamRuby
# authorizations (for facebook, etc -- omniauth) # authorizations (for facebook, etc -- omniauth)
has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization" 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) # connections (websocket-gateway)
has_many :connections, :class_name => "JamRuby::Connection" has_many :connections, :class_name => "JamRuby::Connection"
@ -698,6 +701,20 @@ module JamRuby
end end
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 # given an array of instruments, update a user's instruments
def update_instruments(instruments) def update_instruments(instruments)
# delete all instruments for this user first # delete all instruments for this user first

View File

@ -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

View File

@ -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.new({:name=>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

View File

@ -30,10 +30,10 @@ describe RsvpRequest do
@slot1 = FactoryGirl.build(:rsvp_slot, :music_session => @music_session, :instrument => JamRuby::Instrument.find('electric guitar')) @slot1 = FactoryGirl.build(:rsvp_slot, :music_session => @music_session, :instrument => JamRuby::Instrument.find('electric guitar'))
@slot1.save @slot1.save
@slot2 = FactoryGirl.build(:rsvp_slot, :music_session => @music_session, :instrument => JamRuby::Instrument.find('drums')) @slot2 = FactoryGirl.build(:rsvp_slot, :music_session => @music_session, :instrument => JamRuby::Instrument.find('drums'))
@slot2.save @slot2.save
@invitation = FactoryGirl.build(:invitation, :sender => @session_creator, :receiver => @session_invitee, :music_session => @music_session) @invitation = FactoryGirl.build(:invitation, :sender => @session_creator, :receiver => @session_invitee, :music_session => @music_session)
@invitation.save @invitation.save
end end
@ -53,12 +53,12 @@ describe RsvpRequest do
@music_session.save @music_session.save
RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id]}, @non_session_invitee) 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 end
it "should allow invitee to RSVP to session with closed RSVPs" do 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) rsvp = RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id], :message => "We be jammin!"}, @session_invitee)
# verify comment # verify comment
comment = SessionInfoComment.find_by_creator_id(@session_invitee) comment = SessionInfoComment.find_by_creator_id(@session_invitee)
comment.comment.should == "We be jammin!" comment.comment.should == "We be jammin!"
@ -373,12 +373,14 @@ describe RsvpRequest do
comment = SessionInfoComment.find_by_creator_id(@session_invitee) comment = SessionInfoComment.find_by_creator_id(@session_invitee)
comment.comment.should == "Let's Jam!" comment.comment.should == "Let's Jam!"
# cancel calendar_count = Calendar.find(:all).count
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
# 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 = RsvpRequest.find_by_id(rsvp.id)
rsvp.canceled.should == true rsvp.canceled.should == true
rsvp.cancel_all.should == true rsvp.cancel_all.should == true
(Calendar.find(:all).count - calendar_count).should eq(1)
# verify comment # verify comment
comment = SessionInfoComment.find_by_creator_id(@session_invitee) comment = SessionInfoComment.find_by_creator_id(@session_invitee)

View File

@ -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

View File

@ -10,6 +10,7 @@
var dialogId = 'rsvp-submit-dialog'; var dialogId = 'rsvp-submit-dialog';
var $btnSubmit = $("#btnSubmitRsvp"); var $btnSubmit = $("#btnSubmitRsvp");
function beforeShow(data) { function beforeShow(data) {
$('.error', $dialog).hide(); $('.error', $dialog).hide();
} }
@ -56,7 +57,6 @@
$btnSubmit.unbind('click'); $btnSubmit.unbind('click');
$btnSubmit.click(function(e) { $btnSubmit.click(function(e) {
e.preventDefault(); e.preventDefault();
var error = false; var error = false;
var slotIds = []; var slotIds = [];
var selectedSlots = []; var selectedSlots = [];
@ -96,7 +96,11 @@
if (!error) { if (!error) {
$dialog.triggerHandler(EVENTS.RSVP_SUBMITTED); $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) { .fail(function(xhr, textStatus, errorMessage) {

View File

@ -4,6 +4,16 @@
.session-detail-scroller, .session-detail-scroller,
#account-identity-content-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 { .content-wrapper {
padding:10px 30px; padding:10px 30px;
} }

View File

@ -3,6 +3,33 @@
.rsvp-dialog { .rsvp-dialog {
min-height:initial; 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 { .session-name {
margin:3px 0 0; margin:3px 0 0;

View File

@ -1,7 +1,7 @@
require 'sanitize' require 'sanitize'
class ApiUsersController < ApiController 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, before_filter :auth_user, :only => [:session_settings_show, :session_history_index, :session_user_history_index, :update, :delete,
:liking_create, :liking_destroy, # likes :liking_create, :liking_destroy, # likes
:following_create, :following_show, :following_destroy, # followings :following_create, :following_show, :following_destroy, # followings
@ -15,19 +15,22 @@ class ApiUsersController < ApiController
:share_session, :share_recording, :share_session, :share_recording,
:affiliate_report, :audio_latency, :broadcast_notification] :affiliate_report, :audio_latency, :broadcast_notification]
respond_to :json respond_to :json, :except => :calendar
respond_to :ics, :only => :calendar
def index def index
@users = User.paginate(page: params[:page]) @users = User.paginate(page: params[:page])
respond_with @users, responder: ApiResponder, :status => 200 respond_with @users, responder: ApiResponder, :status => 200
end 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 def show
@user = User.includes([{musician_instruments: :instrument}, @user=lookup_user
{band_musicians: :user},
{genre_players: :genre},
:bands, :instruments, :genres, :jam_track_rights, :affiliate_partner])
.find(params[:id])
respond_with @user, responder: ApiResponder, :status => 200 respond_with @user, responder: ApiResponder, :status => 200
end end
@ -80,10 +83,10 @@ class ApiUsersController < ApiController
respond_with_model(@user, new: true, location: lambda { return api_user_detail_url(@user.id) }) respond_with_model(@user, new: true, location: lambda { return api_user_detail_url(@user.id) })
end end
end end
def profile_save def profile_save
end end
def update def update
@user = User.find(params[:id]) @user = User.find(params[:id])
@ -96,7 +99,7 @@ class ApiUsersController < ApiController
@user.country = params[:country] if params.has_key?(:country) @user.country = params[:country] if params.has_key?(:country)
@user.musician = params[:musician] if params.has_key?(:musician) @user.musician = params[:musician] if params.has_key?(:musician)
@user.update_instruments(params[:instruments].nil? ? [] : params[:instruments]) if params.has_key?(:instruments) @user.update_instruments(params[:instruments].nil? ? [] : params[:instruments]) if params.has_key?(:instruments)
# genres # genres
@user.update_genres(params[:genres].nil? ? [] : params[:genres], GenrePlayer::PROFILE) if params.has_key?(: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) @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[: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[: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.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 = 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.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) @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_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_performance_samples(params[:performance_samples]) if params.has_key?(:performance_samples)
@user.update_calendars(params[:calendars]) if params.has_key?(:calendars)
@user.save @user.save
if @user.errors.any? if @user.errors.any?
@ -196,9 +199,9 @@ class ApiUsersController < ApiController
end end
def delete def delete
@user.destroy @user.destroy
respond_with responder: ApiResponder, :status => 204 respond_with responder: ApiResponder, :status => 204
end end
def signup_confirm def signup_confirm
@user = UserManager.new.signup_confirm(params[:signup_token]) @user = UserManager.new.signup_confirm(params[:signup_token])
@ -260,7 +263,7 @@ class ApiUsersController < ApiController
def auth_session_delete def auth_session_delete
sign_out sign_out
render :json => { :success => true }, :status => 200 render :json => { :success => true }, :status => 200
end end
###################### SESSION SETTINGS ################### ###################### SESSION SETTINGS ###################
def session_settings_show def session_settings_show
@ -276,7 +279,7 @@ class ApiUsersController < ApiController
@session_user_history = @user.session_user_history(params[:id], params[:session_id]) @session_user_history = @user.session_user_history(params[:id], params[:session_id])
end end
###################### BANDS ######################## ###################### BANDS ########################
def band_index def band_index
@bands = User.band_index(params[:id]) @bands = User.band_index(params[:id])
end end
@ -296,7 +299,7 @@ class ApiUsersController < ApiController
@user = User.find(params[:id]) @user = User.find(params[:id])
if !params[:user_id].nil? if !params[:user_id].nil?
@user.create_user_liking(params[:user_id]) @user.create_user_liking(params[:user_id])
elsif !params[:band_id].nil? elsif !params[:band_id].nil?
@user.create_band_liking(params[:band_id]) @user.create_band_liking(params[:band_id])
end end
@ -454,7 +457,7 @@ class ApiUsersController < ApiController
respond_with @invitation, responder: ApiResponder, :status => 200 respond_with @invitation, responder: ApiResponder, :status => 200
rescue ActiveRecord::RecordNotFound 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
end end
@ -467,9 +470,9 @@ class ApiUsersController < ApiController
params[:accepted]) params[:accepted])
respond_with @invitation, responder: ApiResponder, :status => 200 respond_with @invitation, responder: ApiResponder, :status => 200
rescue ActiveRecord::RecordNotFound 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
end end
@ -576,11 +579,11 @@ class ApiUsersController < ApiController
# user_id is deduced if possible from the user's cookie. # user_id is deduced if possible from the user's cookie.
@dump = CrashDump.new @dump = CrashDump.new
@dump.client_type = params[:client_type] @dump.client_type = params[:client_type]
@dump.client_version = params[:client_version] @dump.client_version = params[:client_version]
@dump.client_id = params[:client_id] @dump.client_id = params[:client_id]
@dump.user_id = current_user.try(:id) @dump.user_id = current_user.try(:id)
@dump.session_id = params[:session_id] @dump.session_id = params[:session_id]
@dump.timestamp = params[:timestamp] @dump.timestamp = params[:timestamp]
unless @dump.save unless @dump.save
@ -589,7 +592,7 @@ class ApiUsersController < ApiController
respond_with @dump respond_with @dump
return return
end end
# This part is the piece that really needs to be decomposed into a library... # This part is the piece that really needs to be decomposed into a library...
if Rails.application.config.storage_type == :fog if Rails.application.config.storage_type == :fog
s3 = AWS::S3.new(:access_key_id => Rails.application.config.aws_access_key_id, 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] bucket = s3.buckets[Rails.application.config.aws_bucket]
uri = @dump.uri uri = @dump.uri
expire = Time.now + 20.years expire = Time.now + 20.years
read_url = bucket.objects[uri].url_for(:read, read_url = bucket.objects[uri].url_for(:read,
:expires => expire, :expires => expire,
:'response_content_type' => 'application/octet-stream').to_s :'response_content_type' => 'application/octet-stream').to_s
@dump.update_attribute(:uri, read_url) @dump.update_attribute(:uri, read_url)
write_url = bucket.objects[uri].url_for(:write, write_url = bucket.objects[uri].url_for(:write,
:expires => Rails.application.config.crash_dump_data_signed_url_timeout, :expires => Rails.application.config.crash_dump_data_signed_url_timeout,
:'response_content_type' => 'application/octet-stream').to_s :'response_content_type' => 'application/octet-stream').to_s
logger.debug("crash_dump can read from url #{read_url}") logger.debug("crash_dump can read from url #{read_url}")
redirect_to write_url redirect_to write_url
@ -744,9 +747,9 @@ class ApiUsersController < ApiController
if txt = oo.affiliate_legalese.try(:legalese) if txt = oo.affiliate_legalese.try(:legalese)
txt = ControllerHelp.instance.simple_format(txt) txt = ControllerHelp.instance.simple_format(txt)
end end
result['agreement'] = { result['agreement'] = {
'legalese' => txt, 'legalese' => txt,
'signed_at' => oo.signed_at 'signed_at' => oo.signed_at
} }
#result['signups'] = oo.referrals_by_date #result['signups'] = oo.referrals_by_date
#result['earnings'] = [['April 2015', '1000 units', '$100']] #result['earnings'] = [['April 2015', '1000 units', '$100']]
@ -851,7 +854,7 @@ class ApiUsersController < ApiController
else else
render json: { message: 'Valid Site', data: data }, status: 200 render json: { message: 'Valid Site', data: data }, status: 200
end end
else else
render json: { message: "unknown validation for data '#{params[:data]}', site '#{params[:site]}'" }, status: :unprocessable_entity render json: { message: "unknown validation for data '#{params[:data]}', site '#{params[:site]}'" }, status: :unprocessable_entity
end end
end end
@ -880,6 +883,14 @@ class ApiUsersController < ApiController
render json: { }, status: 200 render json: { }, status: 200
end 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 ####################### ###################### RECORDINGS #######################
# def recording_index # def recording_index
# @recordings = User.recording_index(current_user, params[:id]) # @recordings = User.recording_index(current_user, params[:id])
@ -932,5 +943,5 @@ class ApiUsersController < ApiController
# @recording = Recording.find(params[:recording_id]) # @recording = Recording.find(params[:recording_id])
# @recording.delete # @recording.delete
# respond_with responder: ApiResponder, :status => 204 # respond_with responder: ApiResponder, :status => 204
# end # end
end end

View File

@ -22,6 +22,10 @@
%thead %thead
%tbody %tbody
.clearall .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 / end content scrolling area
%script{type: 'text/template', id: 'template-account-session'} %script{type: 'text/template', id: 'template-account-session'}

View File

@ -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

View File

@ -7,16 +7,27 @@
.session-name .session-name
.scheduled-start .scheduled-start
.schedule-recurrence .schedule-recurrence
.part .rsvp-options
.slot-instructions Check the box(es) next to the track(s) you want to play in the session: .part
.error{:style => 'display:none'} .slot-instructions Check the box(es) next to the track(s) you want to play in the session:
.rsvp-instruments .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 .buttons
.left .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 .right
%a.button-grey{:id => 'btnCancel', 'layout-action' => 'close'} CANCEL %a#btnCancel.button-grey{'layout-action' => 'close'} CANCEL
%a.button-orange{:id => 'btnSubmitRsvp'} SUBMIT RSVP %a#btnSubmitRsvp.button-orange SUBMIT RSVP

View File

@ -271,9 +271,9 @@ SampleApp::Application.routes.draw do
#match '/users' => 'api_users#create', :via => :post #match '/users' => 'api_users#create', :via => :post
match '/users/:id' => 'api_users#update', :via => :post match '/users/:id' => 'api_users#update', :via => :post
match '/users/:id' => 'api_users#delete', :via => :delete 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/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/complete/:signup_token' => 'api_users#complete', as: 'complete', via: 'post'
match '/users/:id/set_password' => 'api_users#set_password', :via => :post match '/users/:id/set_password' => 'api_users#set_password', :via => :post
# recurly # recurly

View File

@ -40,6 +40,11 @@ DailySessionEmailer:
class: "JamRuby::DailySessionEmailer" class: "JamRuby::DailySessionEmailer"
description: "Sends daily scheduled session emails" description: "Sends daily scheduled session emails"
DailyJob:
cron: "0 4 * * *"
class: "JamRuby::DailyJob"
description: "Aggregate task to perform general daily things"
ScheduledMusicSessionCleaner: ScheduledMusicSessionCleaner:
cron: "0 3 * * *" cron: "0 3 * * *"
class: "JamRuby::ScheduledMusicSessionCleaner" class: "JamRuby::ScheduledMusicSessionCleaner"

View File

@ -59,6 +59,27 @@ describe ApiUsersController do
end end
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 describe "update mod" do
it "empty mod" do it "empty mod" do
post :update, id:user.id, mods: {}, :format=>'json' post :update, id:user.id, mods: {}, :format=>'json'
@ -83,13 +104,13 @@ describe ApiUsersController do
end end
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 = Utils::SITE_TYPES.clone << 'bandcamp-fan'
site_types.each do |sitetype| site_types.each do |sitetype|
rec_id = nil rec_id = nil
case sitetype case sitetype
when 'url' when 'url'
valid, invalid = 'http://jamkazam.com', 'http://jamkazamxxx.com' valid, invalid = 'http://jamkazam.com', 'http://jamkazamxxx.com'
when 'youtube' when 'youtube'