diff --git a/db/manifest b/db/manifest index 36e365013..5bad3d1e0 100755 --- a/db/manifest +++ b/db/manifest @@ -182,3 +182,4 @@ rsvp_slots_prof_level.sql add_file_name_music_notation.sql change_scheduled_start_music_session.sql music_sessions_iso_639_3.sql +discardable_claimed_recordings.sql diff --git a/db/up/discardable_claimed_recordings.sql b/db/up/discardable_claimed_recordings.sql new file mode 100644 index 000000000..d082ff770 --- /dev/null +++ b/db/up/discardable_claimed_recordings.sql @@ -0,0 +1 @@ +ALTER TABLE claimed_recordings ADD COLUMN discarded BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/ruby/lib/jam_ruby/lib/nav.rb b/ruby/lib/jam_ruby/lib/nav.rb index 56a7a09af..1bce0f133 100644 --- a/ruby/lib/jam_ruby/lib/nav.rb +++ b/ruby/lib/jam_ruby/lib/nav.rb @@ -1,9 +1,18 @@ module JamRuby + # To use this, be sure to add this in before block: stub_const("APP_CONFIG", web_config) class Nav def self.home(options ={}) - "#{APP_CONFIG.external_root_url}/client#/home#{dialog(options)}" + "#{base_url}/home#{dialog(options)}" + end + + def self.profile(user) + "#{base_url}/profile/#{user.id}" + end + + def self.feed + "#{base_url}/feed" end def self.accept_friend_request_dialog(friend_request_id) @@ -12,6 +21,10 @@ module JamRuby private + def self.base_url + "#{APP_CONFIG.external_root_url}/client#" + end + def self.dialog(options) dialog = '' if options[:dialog] diff --git a/ruby/lib/jam_ruby/models/claimed_recording.rb b/ruby/lib/jam_ruby/models/claimed_recording.rb index 573d5a390..d984d6a3c 100644 --- a/ruby/lib/jam_ruby/models/claimed_recording.rb +++ b/ruby/lib/jam_ruby/models/claimed_recording.rb @@ -8,9 +8,9 @@ module JamRuby belongs_to :genre, :class_name => "JamRuby::Genre" has_many :recorded_tracks, :through => :recording, :class_name => "JamRuby::RecordedTrack" has_many :playing_sessions, :class_name => "JamRuby::ActiveMusicSession" - has_many :likes, :class_name => "JamRuby::RecordingLiker", :foreign_key => "claimed_recording_id" + has_many :likes, :class_name => "JamRuby::RecordingLiker", :foreign_key => "claimed_recording_id", :dependent => :destroy has_many :plays, :class_name => "JamRuby::PlayablePlay", :foreign_key => "claimed_recording_id", :dependent => :destroy - has_one :share_token, :class_name => "JamRuby::ShareToken", :inverse_of => :shareable, :foreign_key => 'shareable_id' + has_one :share_token, :class_name => "JamRuby::ShareToken", :inverse_of => :shareable, :foreign_key => 'shareable_id', :dependent => :destroy validates :name, no_profanity: true, length: {minimum: 3, maximum: 64}, presence: true validates :description, no_profanity: true, length: {maximum: 8000} @@ -39,9 +39,9 @@ module JamRuby raise PermissionError, "user doesn't own claimed_recording" end - self.name = params[:name] unless params[:name].nil? - self.description = params[:description] unless params[:description].nil? - self.genre = Genre.find(params[:genre]) unless params[:genre].nil? + self.name = params[:name] + self.description = params[:description] + self.genre = Genre.find_by_id(params[:genre]) unless params[:genre].nil? self.is_public = params[:is_public] unless params[:is_public].nil? save end @@ -50,13 +50,10 @@ module JamRuby if user != self.user raise PermissionError, "user doesn't own claimed_recording" end - - # If this is the only copy, destroy the entire recording. Otherwise, just destroy this claimed_recording - if recording.claimed_recordings.count == 1 - recording.destroy - else - self.destroy - end + + ClaimedRecording.where(:id => id).update_all(:discarded => true ) + + recording.discard(user) end diff --git a/ruby/lib/jam_ruby/models/feed.rb b/ruby/lib/jam_ruby/models/feed.rb index 7c118d364..0802e3085 100644 --- a/ruby/lib/jam_ruby/models/feed.rb +++ b/ruby/lib/jam_ruby/models/feed.rb @@ -43,6 +43,7 @@ module JamRuby query = Feed.joins("LEFT OUTER JOIN recordings ON recordings.id = feeds.recording_id") .joins("LEFT OUTER JOIN music_sessions ON music_sessions.id = feeds.music_session_id") .limit(limit) + .where('recordings is NULL OR recordings.all_discarded = false') # remove any 'all_discarded recordings from the search results' # handle sort if sort == 'date' @@ -72,14 +73,14 @@ module JamRuby end - if target_user + if target_user if target_user != user.id require_public_recordings = "claimed_recordings.is_public = TRUE AND" require_public_sessions = "music_sessions.fan_access = TRUE AND" end - query = query.joins("LEFT OUTER JOIN claimed_recordings ON recordings.id = claimed_recordings.recording_id AND #{require_public_recordings} (claimed_recordings.user_id = '#{target_user}' OR (recordings.band_id IN (SELECT band_id FROM bands_musicians where user_id='#{target_user}')))") + query = query.joins("LEFT OUTER JOIN claimed_recordings ON recordings.id = claimed_recordings.recording_id AND claimed_recordings.discarded = FALSE AND #{require_public_recordings} (claimed_recordings.user_id = '#{target_user}' OR (recordings.band_id IN (SELECT band_id FROM bands_musicians where user_id='#{target_user}')))") query = query.joins("LEFT OUTER JOIN music_sessions_user_history ON music_sessions.id = music_sessions_user_history.music_session_id AND #{require_public_sessions} music_sessions_user_history.user_id = '#{target_user}'") query = query.group("feeds.id, feeds.recording_id, feeds.music_session_id, feeds.created_at, feeds.updated_at, recordings.id, music_sessions.id") if sort == 'plays' @@ -97,7 +98,7 @@ module JamRuby require_public_sessions = "music_sessions.fan_access = TRUE AND" end - query = query.joins("LEFT OUTER JOIN claimed_recordings ON recordings.id = claimed_recordings.recording_id AND #{require_public_recordings} recordings.band_id = '#{target_band}'") + query = query.joins("LEFT OUTER JOIN claimed_recordings ON recordings.id = claimed_recordings.recording_id AND claimed_recordings.discarded = FALSE AND #{require_public_recordings} recordings.band_id = '#{target_band}'") query = query.where("music_sessions IS NULL OR #{require_public_sessions} music_sessions.band_id = '#{target_band}'") query = query.group("feeds.id, feeds.recording_id, feeds.music_session_id, feeds.created_at, feeds.updated_at, recordings.id, music_sessions.id") if sort == 'plays' @@ -108,7 +109,7 @@ module JamRuby query = query.where('recordings.id is NULL OR claimed_recordings.id IS NOT NULL') #query = query.where('music_sessions.id is NULL OR music_sessions_user_history.id IS NOT NULL') else - query = query.joins('LEFT OUTER JOIN claimed_recordings ON recordings.id = claimed_recordings.recording_id AND claimed_recordings.is_public = TRUE') + query = query.joins('LEFT OUTER JOIN claimed_recordings ON recordings.id = claimed_recordings.recording_id AND claimed_recordings.discarded = FALSE AND claimed_recordings.is_public = TRUE') query = query.joins("LEFT OUTER JOIN music_sessions_user_history ON music_sessions.id = music_sessions_user_history.music_session_id AND music_sessions.fan_access = TRUE") query = query.group("feeds.id, feeds.recording_id, feeds.music_session_id, feeds.created_at, feeds.updated_at, recordings.id, music_sessions.id") if sort == 'plays' diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb index 143aed8f6..c4d5fc08d 100644 --- a/ruby/lib/jam_ruby/models/recording.rb +++ b/ruby/lib/jam_ruby/models/recording.rb @@ -5,12 +5,12 @@ module JamRuby attr_accessible :owner, :owner_id, :band, :band_id, :recorded_tracks_attributes, :mixes_attributes, :claimed_recordings_attributes, :name, :description, :genre, :is_public, :duration, as: :admin - has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :recording, :foreign_key => 'recording_id', :dependent => :destroy has_many :users, :through => :recorded_tracks, :class_name => "JamRuby::User" + has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :recording, :foreign_key => 'recording_id', :dependent => :destroy has_many :mixes, :class_name => "JamRuby::Mix", :inverse_of => :recording, :foreign_key => 'recording_id', :dependent => :destroy has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id, :dependent => :destroy - has_many :comments, :class_name => "JamRuby::RecordingComment", :foreign_key => "recording_id" - has_many :likes, :class_name => "JamRuby::RecordingLiker", :foreign_key => "recording_id" + has_many :comments, :class_name => "JamRuby::RecordingComment", :foreign_key => "recording_id", :dependent => :destroy + has_many :likes, :class_name => "JamRuby::RecordingLiker", :foreign_key => "recording_id", :dependent => :destroy has_many :plays, :class_name => "JamRuby::PlayablePlay", :as => :playable, :dependent => :destroy has_one :feed, :class_name => "JamRuby::Feed", :inverse_of => :recording, :foreign_key => 'recording_id', :dependent => :destroy @@ -128,7 +128,7 @@ module JamRuby unless self.users.exists?(user) raise PermissionError, "user was not in this session" end - recorded_tracks.where(:user_id=> user.id) + recorded_tracks.where(:user_id => user.id) end def has_access?(user) @@ -211,7 +211,7 @@ module JamRuby # check if all recorded_tracks for this recording are discarded if recorded_tracks.where('discard = false or discard is NULL').length == 0 - self.all_discarded = true + self.all_discarded = true # the feed won't pick this up; also background cleanup will find these and whack them later self.save(:validate => false) end @@ -238,7 +238,7 @@ module JamRuby .order('recorded_tracks.id') .where('recorded_tracks.fully_uploaded = TRUE') .where('recorded_tracks.id > ?', since) - .where('claimed_recordings.user_id = ?', user).limit(limit).each do |recorded_track| + .where('claimed_recordings.user_id = ? AND claimed_recordings.discarded = FALSE', user).limit(limit).each do |recorded_track| downloads.push( { :type => "recorded_track", @@ -258,7 +258,7 @@ module JamRuby .order('mixes.id') .where('mixes.completed_at IS NOT NULL') .where('mixes.id > ?', since) - .where('claimed_recordings.user_id = ?', user) + .where('claimed_recordings.user_id = ? AND claimed_recordings.discarded = FALSE', user) .limit(limit).each do |mix| downloads.push( { diff --git a/ruby/spec/jam_ruby/models/recording_spec.rb b/ruby/spec/jam_ruby/models/recording_spec.rb index d61a1eecd..0bc9702e0 100644 --- a/ruby/spec/jam_ruby/models/recording_spec.rb +++ b/ruby/spec/jam_ruby/models/recording_spec.rb @@ -188,8 +188,12 @@ describe Recording do expect { @claimed_recordign.discard(@user2) }.to raise_error @claimed_recording = @recording.claim(@user2, "name2", "description2", @genre, true) @claimed_recording.discard(@user2) + @claimed_recording.reload + @claimed_recording.discarded.should == true + @recording.recorded_tracks_for_user(@user2)[0].discard.should == true @recording.reload - @recording.claimed_recordings.length.should == 1 + @recording.claimed_recordings.length.should == 2 + @recording.all_discarded.should == false end it "should destroy the entire recording if there was only one claimed_recording which is discarded" do @@ -199,8 +203,10 @@ describe Recording do @genre = FactoryGirl.create(:genre) @claimed_recording = @recording.claim(@user, "name", "description", @genre, true) @claimed_recording.discard(@user) - expect { Recording.find(@recording.id) }.to raise_error - expect { ClaimedRecording.find(@claimed_recording.id) }.to raise_error + @claimed_recording.reload + @claimed_recording.discarded.should == true + @claimed_recording.recording.all_discarded.should == true + @recording.recorded_tracks_for_user(@user)[0].discard.should == true end it "should use the since parameter when restricting uploads" do diff --git a/web/app/assets/javascripts/banner.js b/web/app/assets/javascripts/banner.js index 7eda9fa0f..99e0ddfbd 100644 --- a/web/app/assets/javascripts/banner.js +++ b/web/app/assets/javascripts/banner.js @@ -9,6 +9,9 @@ var self = this; var logger = context.JK.logger; var $banner = null; + var $closeBtn = null; + var $yesBtn = null; + var $noBtn = null; // you can also do // * showAlert('title', 'text') @@ -26,6 +29,19 @@ return show(options); } + function showYesNo(options) { + if (typeof options == 'string' || options instanceof String) { + if(arguments.length == 2) { + options = {title: options, html:arguments[1]} + } + else { + options = {html:options}; + } + } + options.type = 'yes_no' + return show(options); + } + // responsible for updating the contents of the update dialog // as well as registering for any event handlers function show(options) { @@ -33,7 +49,12 @@ var html = options.html; if(!options.title) { - options.title = 'alert' + if(options.type == 'alert') { + options.title = 'alert' + } + else if(options.type == 'yes_no') { + options.title = 'please confirm'; + } } var $h1 = $banner.find('h1'); @@ -50,11 +71,18 @@ throw "unable to show banner for empty message"; } - var $closeBtn = $banner.find('.close-btn'); if((options.type == "alert" && !options.buttons) || options.close) { - $closeBtn.show().click(function() { + var closeButtonText = 'CLOSE'; + if(options.close !== null && typeof options.close == 'object') { + // extra styling options for close button + if(options.close.name) { + closeButtonText = options.close.name; + } + } + + $closeBtn.show().text(closeButtonText).unbind('click').click(function() { hide(); return false; }); @@ -63,6 +91,27 @@ $closeBtn.hide(); } + if(options.type == "yes_no") { + $yesBtn.show().unbind('click').click(function() { + if(options.yes) { + options.yes(); + } + hide(); + return false; + }) + $noBtn.show().unbind('click').click(function() { + if(options.no) { + options.no(); + } + hide(); + return false; + }) + } + else { + $yesBtn.hide(); + $noBtn.hide(); + } + if(options.buttons) { var $buttons = $banner.find('.buttons') context._.each(options.buttons, function(button) { @@ -86,6 +135,7 @@ return newContent; } + function hide() { $banner.hide(); $banner.find('.user-btn').remove(); @@ -96,6 +146,10 @@ function initialize() { $banner = $('#banner'); + + $closeBtn = $banner.find('.close-btn'); + $yesBtn = $banner.find('.yes-btn'); + $noBtn = $banner.find('.no-btn'); return self; } @@ -104,6 +158,7 @@ initialize: initialize, show: show, showAlert: showAlert, + showYesNo: showYesNo,// shows Yes and Cancel button (confirmation dialog) hide: hide } diff --git a/web/app/assets/javascripts/commentDialog.js b/web/app/assets/javascripts/commentDialog.js index 263ea7d6d..794ee141b 100644 --- a/web/app/assets/javascripts/commentDialog.js +++ b/web/app/assets/javascripts/commentDialog.js @@ -122,7 +122,7 @@ } function showDialog() { - app.layout.showDialog('comment-dialog'); + return app.layout.showDialog('comment-dialog'); } function initialize() { diff --git a/web/app/assets/javascripts/editRecordingDialog.js b/web/app/assets/javascripts/editRecordingDialog.js new file mode 100644 index 000000000..23c1d4919 --- /dev/null +++ b/web/app/assets/javascripts/editRecordingDialog.js @@ -0,0 +1,170 @@ +(function (context, $) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.EditRecordingDialog = function (app) { + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var claimedRecordingId = null; + var $dialog = null; + var $form = null; + var $name = null; + var $description = null; + var $genre = null; + var $isPublic = null; + var $cancelBtn = null; + var $saveBtn = null; + var $deleteBtn = null; + + var updating = false; + var deleting = false; + + function resetForm() { + + // remove all display errors + $dialog.find('.error-text').remove() + $dialog.find('.error').removeClass("error") + } + + function beforeShow(args) { + + claimedRecordingId = args.d1; + + if(!claimedRecordingId) throw "claimedRecordingId must be specified"; + + resetForm(); + + rest.getClaimedRecording(claimedRecordingId) + .done(function(data) { + var name = data.name; + var description = data.description; + var is_public = data.is_public; + var genre_id = data.genre_id; + + context.JK.GenreSelectorHelper.setSelectedGenres($genre.parent(), [genre_id]); + $name.val(name); + $description.val(description); + if(is_public) { + $isPublic.attr('checked', 'checked').iCheck('check') + } + else { + $isPublic.removeAttr('checked').iCheck('uncheck') + } + }) + .fail(app.ajaxError) + } + + function afterHide() { + + } + + function attemptUpdate() { + if(updating) return; + + updating = true; + var name = $name.val(); + var description = $description.val(); + var genre = $genre.val(); + var is_public = $isPublic.is(':checked'); + + rest.updateClaimedRecording({id: claimedRecordingId, name: name, description: description, is_public: is_public, genre: genre }) + .done(function(updated) { + resetForm(); + $dialog.triggerHandler('recording_updated', {id: claimedRecordingId, name: name, description: description, is_public: is_public, genre: genre}) + app.layout.closeDialog('edit-recording'); + }) + .fail(function(jqXHR) { + + if(jqXHR.status = 422) { + // highlight fields in error + + resetForm(); + + var errors = JSON.parse(jqXHR.responseText); + + var $name_errors = context.JK.format_errors('name', errors); + if ($name_errors) $name.closest('div.field').addClass('error').end().after($name_errors); + + var $description_errors = context.JK.format_errors('description', errors); + if ($description_errors) $description.closest('div.field').addClass('error').end().after($description_errors); + + var $genre_errors = context.JK.format_errors('genre', errors); + if ($genre_errors) $genre.closest('div.field').addClass('error').end().after($genre_errors); + + var $is_public_errors = context.JK.format_errors('is_public', errors); + if ($is_public_errors) $isPublic.closest('div.field').addClass('error').end().after($is_public_errors); + + } + else { + app.ajaxError(arguments); + } + }) + .always(function() { + updating = false; + }) + } + + function attemptDelete() { + if(deleting) return; + + deleting = true; + context.JK.Banner.showYesNo({ + title: "Confirm Deletion", + html: "Are you sure you want to delete this recording?", + yes: function() { + rest.deleteClaimedRecording(claimedRecordingId) + .done(function() { + $dialog.triggerHandler('recording_deleted', {id: claimedRecordingId}); + app.layout.closeDialog('edit-recording'); + }) + .fail(app.ajaxError) + .always(function() { + deleting = false; + }) + }, + no : function() { + context.JK.Banner.hide(); + deleting = false; + } + }) + } + + function cancel() { + app.layout.closeDialog('edit-recording'); + } + + function events() { + $saveBtn.click(attemptUpdate); + $deleteBtn.click(attemptDelete); + $cancelBtn.click(cancel) + $form.submit(false); + } + + + function initialize() { + var dialogBindings = { + 'beforeShow': beforeShow, + 'afterHide': afterHide + }; + + app.bindDialog('edit-recording', dialogBindings); + + $dialog = $('#edit-recording-dialog'); + $form = $dialog.find('form'); + $cancelBtn = $dialog.find('.cancel-btn'); + $saveBtn = $dialog.find('.save-btn'); + $deleteBtn = $dialog.find('.delete-btn'); + $name = $dialog.find('input[name="name"]'); + $description = $dialog.find('textarea[name="description"]'); + $genre = $dialog.find('select[name=genre]'); + $isPublic = $dialog.find('input[name=is_public]'); + + events(); + + context.JK.GenreSelectorHelper.render($genre.parent()); + context.JK.checkbox($isPublic); + }; + + this.initialize = initialize; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/feed.js b/web/app/assets/javascripts/feed.js index 486b9e604..41027b898 100644 --- a/web/app/assets/javascripts/feed.js +++ b/web/app/assets/javascripts/feed.js @@ -27,7 +27,7 @@ var $scroller = $screen.find('.content-body-scroller'); var $content = $screen.find('.feed-content'); var $noMoreFeeds = $('#end-of-feeds-list'); - var $refresh = $screen.find('#btn-refresh-feed'); + var $refresh = $screen.find('.btn-refresh-entries'); var $sortFeedBy = $screen.find('#feed_order_by'); var $includeDate = $screen.find('#feed_date'); var $includeType = $screen.find('#feed_show'); diff --git a/web/app/assets/javascripts/feedHelper.js b/web/app/assets/javascripts/feedHelper.js index 088a8c37e..fd7b3eb73 100644 --- a/web/app/assets/javascripts/feedHelper.js +++ b/web/app/assets/javascripts/feedHelper.js @@ -7,7 +7,9 @@ var logger = context.JK.logger; var rest = new context.JK.Rest(); + var EVENTS = context.JK.EVENTS; var ui = new context.JK.UIHelper(JK.app); + var userId = null; var currentQuery = null; var currentPage = 0; var LIMIT = 20; @@ -29,6 +31,10 @@ query.since = next; } + if(userId) { + query.user = userId; + } + return query; } @@ -61,6 +67,10 @@ $scroller.infinitescroll('pause'); logger.debug("end of feeds") + if(currentPage == 0 && response.entries.length == 0) { + $content.append("
This user has no history.
") ; + } + if(currentPage > 0) { $noMoreFeeds.show(); // there are bugs with infinitescroll not removing the 'loading'. @@ -75,6 +85,10 @@ } } + function setUser(_userId) { + userId = _userId; + } + function refresh() { clearResults(); @@ -221,6 +235,49 @@ return false; } + function isOwner() { + return userId == context.JK.currentUserId; + } + + function obtainCandidate(recording) { + if(isOwner()) { + var candidate = null; + context._.each(recording.claimed_recordings, function(claimedRecording) { + if(claimedRecording.user_id == context.JK.currentUserId) { + candidate = claimedRecording; + return false; + } + }) + + if(!candidate) throw "unable to find candidate claimed recording, yet we can see this recording. server error..." + return candidate; + } + else { + return recording.claimed_recordings[0] + } + } + + function toggleOpen($feedItem, $name, $description, $musicians) { + $description.trigger('destroy.dot'); + $description.data('original-height', $description.css('height')).css('height', 'auto'); + $name.trigger('destroy.dot'); + $name.data('original-height', $name.css('height')).css('height', 'auto'); + $musicians.show(); + $feedItem.animate({'max-height': '1000px'}); + } + + function toggleClose($feedItem, $name, $description, $musicians, immediate) { + $feedItem.css('height', $feedItem.height() + 'px') + $feedItem.animate({'height': $feedItem.data('original-max-height')}, immediate ? 0 : 400).promise().done(function() { + $feedItem.css('height', 'auto').css('max-height', $feedItem.data('original-max-height')); + + $musicians.hide(); + $description.css('height', $description.data('original-height')); + $description.dotdotdot(); + $name.css('height', $name.data('original-height')); + $name.dotdotdot(); + }); + } function stateChangeRecording(e, data) { var $controls = data.element; @@ -251,24 +308,10 @@ var toggledOpen = $detailsLink.data('toggledOpen'); if(toggledOpen) { - $feedItem.css('height', $feedItem.height() + 'px') - $feedItem.animate({'height': $feedItem.data('original-max-height')}).promise().done(function() { - $feedItem.css('height', 'auto').css('max-height', $feedItem.data('original-max-height')); - - $musicians.hide(); - $description.css('height', $description.data('original-height')); - $description.dotdotdot(); - $name.css('height', $name.data('original-height')); - $name.dotdotdot(); - }); + toggleClose($feedItem, $name, $description, $musicians) } else { - $description.trigger('destroy.dot'); - $description.data('original-height', $description.css('height')).css('height', 'auto'); - $name.trigger('destroy.dot'); - $name.data('original-height', $name.css('height')).css('height', 'auto'); - $musicians.show(); - $feedItem.animate({'max-height': '1000px'}); + toggleOpen($feedItem, $name, $description, $musicians) } toggledOpen = !toggledOpen; @@ -277,6 +320,64 @@ return false; } + function updateRecordingName($feedEntry, name) { + $feedEntry.find('.name-text').text(name); + } + + function updateRecordingDescription($feedEntry, description) { + $feedEntry.find('.description').text(description); + } + + function updateIsPublic($feedEntry, isPublic) { + var $isPrivate = $feedEntry.find('.is_private') + if(isPublic) { + $isPrivate.removeClass('enabled') + } + else { + $isPrivate.addClass('enabled') + } + } + + function updateComments($feedEntry, comments) { + $feedEntry.find('.comments').html(comments) + } + + function updatePlays($feedEntry, plays) { + $feedEntry.find('.plays').html(plays); + } + + function updateLikes($feedEntry, likes) { + $feedEntry.find('.likes').html(likes); + } + + function updateStats($feedEntry) { + if($feedEntry.is('.recording-entry')) { + var id = $feedEntry.attr('data-claimed-recording-id'); + rest.getClaimedRecording(id) + .done(function(claimedRecording) { + updateComments($feedEntry, claimedRecording.recording.comment_count); + updateLikes($feedEntry, claimedRecording.recording.like_count); + updatePlays($feedEntry, claimedRecording.recording.play_count); + + }) + .fail(app.ajaxError) + } + else { + var id = $feedEntry.attr('data-music-session'); + rest.getSessionHistory(id) + .done(function(music_session) { + updateComments($feedEntry, music_session.comment_count); + updateLikes($feedEntry, music_session.like_count); + updatePlays($feedEntry, music_session.play_count); + }) + .fail(app.ajaxError) + } + } + + function updateGenre($feedEntry, genre) { + $feedEntry.find('.genre').text(context.JK.GenreSelectorHelper.getNameForId(genre)); + } + function renderFeeds(feeds) { $.each(feeds.entries, function(i, feed) { @@ -308,10 +409,12 @@ } $('.btn-comment', $feedItem).click(function() { - ui.launchCommentDialog({ + var result = ui.launchCommentDialog({ session_id: feed.id, entity_type: 'session' - }); + }).one(EVENTS.DIALOG_CLOSED, function() { + updateStats($feedItem); + }) }); $('.btn-like', $feedItem).click(function() { @@ -336,7 +439,7 @@ } var options = { feed_item: feed, - candidate_claimed_recording: feed.claimed_recordings[0], + candidate_claimed_recording: obtainCandidate(feed), mix_class: feed['has_mix?'] ? 'has-mix' : 'no-mix', } @@ -349,6 +452,7 @@ $('.details', $feedItem).click(toggleRecordingDetails); $('.details-arrow', $feedItem).click(toggleRecordingDetails); $('.play-button', $feedItem).click(toggleRecordingPlay); + updateIsPublic($feedItem, options.candidate_claimed_recording.is_public); $('.btn-share', $feedItem).click(function() { ui.launchShareDialog(options.candidate_claimed_recording.id, 'recording'); @@ -359,6 +463,9 @@ recording_id: feed.id, claimed_recording_id: options.candidate_claimed_recording.id, entity_type: 'recording' + }) + .one(EVENTS.DIALOG_CLOSED, function() { + updateStats($feedItem); }); }); @@ -366,6 +473,47 @@ ui.addRecordingLike(feed.id, options.candidate_claimed_recording.id, JK.currentUserId, $('.likes', $feedItem), $('.btn-like', $feedItem)); }); + if(isOwner()) { + $('.edit-recording-dialog', $feedItem).data('claimed_recording_id', options.candidate_claimed_recording.id).click(function() { + app.layout.showDialog('edit-recording', {d1: $(this).data('claimed_recording_id')}) + .one(EVENTS.DIALOG_CLOSED, function() { + $(this).unbind('recording_updated').unbind('recording_deleted'); + }) + .one('recording_updated', function(e, data) { + // find recording by claimed recording id + var $feedEntry = $screen.find('.feed-entry.recording-entry[data-claimed-recording-id="'+ data.id +'"]'); + var $musicians = $feedEntry.find('.musician-detail'); + var $description = $feedEntry.find('.description'); + var $name = $feedEntry.find('.name'); + var $detailsLink = $feedEntry.find('.details'); + var toggledOpen = $detailsLink.data('toggledOpen'); + + if(toggledOpen) { + toggleClose($feedEntry, $name, $description, $musicians, true); + } + + $description.trigger('destroy.dot'); + $name.trigger('destroy.dot'); + + updateRecordingName($feedEntry, data.name); + updateRecordingDescription($feedEntry, data.description); + updateIsPublic($feedEntry, data.is_public); + updateGenre($feedEntry, data.genre); + + $name.dotdotdot(); + $description.dotdotdot(); + $feedItem.data('original-max-height', $feedEntry.css('height')); + + $detailsLink.data('toggledOpen', false); + }) + .one('recording_deleted', function(e, data) { + var $feedEntry = $screen.find('.feed-entry.recording-entry[data-claimed-recording-id="'+ data.id +'"]'); + $feedEntry.remove(); + }) + return false; + }).show(); + } + // put the feed item on the page renderFeed($feedItem); @@ -431,6 +579,7 @@ this.initialize = initialize; this.refresh = refresh; + this.setUser = setUser; return this; } diff --git a/web/app/assets/javascripts/genreSelector.js b/web/app/assets/javascripts/genreSelector.js index 7dabdf2b0..42c2ca442 100644 --- a/web/app/assets/javascripts/genreSelector.js +++ b/web/app/assets/javascripts/genreSelector.js @@ -7,21 +7,12 @@ "use strict"; context.JK = context.JK || {}; + context.JK.GenreSelectorDeferred = null; context.JK.GenreSelectorHelper = (function() { var logger = context.JK.logger; var _genres = []; // will be list of structs: [ {label:xxx, value:yyy}, {...}, ... ] - function loadGenres() { - var url = "/api/genres"; - $.ajax({ - type: "GET", - url: url, - async: false, // do this synchronously so the event handlers in events() can be wired up - success: genresLoaded - }); - } - function reset(parentSelector, defaultGenre) { defaultGenre = typeof(defaultGenre) == 'undefined' ? '' : defaultGenre; $('select', parentSelector).val(defaultGenre); @@ -38,7 +29,7 @@ function render(parentSelector) { $('select', parentSelector).empty(); - $('select', parentSelector).append(''); + $('select', parentSelector).append(''); var template = $('#template-genre-option').html(); $.each(_genres, function(index, value) { // value will be a dictionary entry from _genres: @@ -67,6 +58,21 @@ return selectedGenres; } + function getNameForId(genreId) { + var name = null; + context._.each(_genres, function(genre) { + if(genreId == genre.value) { + name = genre.label; + return false; + } + }); + + if(!name) { + logger.warn("no genre found for genreId: " + genreId); + } + return name; + } + function setSelectedGenres(parentSelector, genreList) { if (!genreList) { return; @@ -79,18 +85,31 @@ $('select', parentSelector).val(values[0]); } - function initialize() { - loadGenres(); + function initialize(app) { + // XXX; _instruments should be populated in a template, rather than round-trip to server + if(!context.JK.GenreSelectorDeferred) { + // this dance is to make sure there is only one server request instead of InstrumentSelector instances * + context.JK.GenreSelectorDeferred = rest.getGenres() + } + + context.JK.GenreSelectorDeferred + .done(function(response) {genresLoaded(response)}) + .fail(app.ajaxError) + + return this; } var me = { // This will be our singleton. initialize: initialize, getSelectedGenres: getSelectedGenres, setSelectedGenres: setSelectedGenres, + getNameForId : getNameForId, getSelectedGenresValues: getSelectedGenresValues, reset: reset, - render: render, - loadGenres: loadGenres + render: function() { + var _args = arguments; + context.JK.GenreSelectorDeferred.done(function(){render.apply(self, _args)}) + } }; return me; diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 2d331be18..3bc8556bc 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -948,6 +948,27 @@ }); } + function updateClaimedRecording(options) { + var claimedRecordingId = options["id"]; + return $.ajax({ + type: "PUT", + dataType: "json", + url: '/api/claimed_recordings/' + claimedRecordingId, + contentType: 'application/json', + processData: false, + data: JSON.stringify(options) + }); + } + + function deleteClaimedRecording(id) { + return $.ajax({ + type: "DELETE", + dataType: "json", + contentType: 'application/json', + url: "/api/claimed_recordings/" + id + }); + } + function claimRecording(options) { var recordingId = options["id"]; @@ -1213,6 +1234,8 @@ this.getRecording = getRecording; this.getClaimedRecordings = getClaimedRecordings; this.getClaimedRecording = getClaimedRecording; + this.updateClaimedRecording = updateClaimedRecording; + this.deleteClaimedRecording = deleteClaimedRecording; this.claimRecording = claimRecording; this.startPlayClaimedRecording = startPlayClaimedRecording; this.stopPlayClaimedRecording = stopPlayClaimedRecording; diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index ccbff33e1..43aaf0d7b 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -646,6 +646,7 @@ if (dialogEvent(dialog, 'beforeShow', options) === false) { return null; } + logger.debug("opening dialog: " + dialog) var $overlay = $('.dialog-overlay') if (opts.sizeOverlayToContent) { diff --git a/web/app/assets/javascripts/profile.js b/web/app/assets/javascripts/profile.js index b8e2a5810..bc6277302 100644 --- a/web/app/assets/javascripts/profile.js +++ b/web/app/assets/javascripts/profile.js @@ -13,6 +13,7 @@ var sentFriendRequest = false; var profileScreen = null; var textMessageDialog = null; + var feed = null; var instrument_logo_map = context.JK.getInstrumentIconMap24(); @@ -30,6 +31,7 @@ function beforeShow(data) { userId = data.id; + feed.setUser(userId); } function afterShow(data) { @@ -37,6 +39,10 @@ resetForm(); } + function beforeHide(data) { + feed.setUser(null); + } + function resetForm() { $('#profile-instruments').empty(); @@ -571,7 +577,7 @@ } function bindHistory() { - + feed.refresh(); } /****************** BANDS TAB *****************/ @@ -752,15 +758,31 @@ function bindFavorites() { } + function initializeFeed() { + + var $scroller = profileScreen.find('.content-body-scroller'); + var $content = profileScreen.find('.feed-content'); + var $noMoreFeeds = $('#end-of-feeds-list'); + var $refresh = profileScreen.find('.btn-refresh-entries'); + var $sortFeedBy = profileScreen.find('#feed_order_by'); + var $includeDate = profileScreen.find('#feed_date'); + var $includeType = profileScreen.find('#feed_show'); + + feed = new context.JK.Feed(app); + feed.initialize(profileScreen, $scroller, $content, $noMoreFeeds, $refresh, $sortFeedBy, $includeDate, $includeType); + } + function initialize(textMessageDialogInstance) { textMessageDialog = textMessageDialogInstance; var screenBindings = { 'beforeShow': beforeShow, - 'afterShow': afterShow + 'afterShow': afterShow, + 'beforeHide' : beforeHide }; app.bindScreen('profile', screenBindings); profileScreen = $('#user-profile'); events(); + initializeFeed(); } this.initialize = initialize; diff --git a/web/app/assets/javascripts/recordingFinishedDialog.js b/web/app/assets/javascripts/recordingFinishedDialog.js index 90061b63e..972badf3e 100644 --- a/web/app/assets/javascripts/recordingFinishedDialog.js +++ b/web/app/assets/javascripts/recordingFinishedDialog.js @@ -98,10 +98,10 @@ id: recording.id }) .done(function () { - console.error("recording discarded by user. recordingId=%o", recording.id); + logger.debug("recording discarded by user. recordingId=%o", recording.id); }) .fail(function (jqXHR) { - console.error("recording discard by user failed. recordingId=%o. reason: %o", recording.id, jqXHR.responseText); + logger.error("recording discard by user failed. recordingId=%o. reason: %o", recording.id, jqXHR.responseText); }) .always(function () { app.layout.closeDialog('recordingFinished') diff --git a/web/app/assets/javascripts/rsvpCancelDialog.js b/web/app/assets/javascripts/rsvpCancelDialog.js index a0a610cb5..2156cec2e 100644 --- a/web/app/assets/javascripts/rsvpCancelDialog.js +++ b/web/app/assets/javascripts/rsvpCancelDialog.js @@ -34,7 +34,7 @@ } function showDialog() { - app.layout.showDialog('rsvp-cancel-dialog'); + return app.layout.showDialog('rsvp-cancel-dialog'); } function events() { diff --git a/web/app/assets/javascripts/rsvpSubmitDialog.js b/web/app/assets/javascripts/rsvpSubmitDialog.js index 281b8120b..9f7ea2e59 100644 --- a/web/app/assets/javascripts/rsvpSubmitDialog.js +++ b/web/app/assets/javascripts/rsvpSubmitDialog.js @@ -55,7 +55,7 @@ } function showDialog() { - app.layout.showDialog(dialogId); + return app.layout.showDialog(dialogId); } function events() { diff --git a/web/app/assets/javascripts/shareDialog.js b/web/app/assets/javascripts/shareDialog.js index dc3bca311..0d3fbcfda 100644 --- a/web/app/assets/javascripts/shareDialog.js +++ b/web/app/assets/javascripts/shareDialog.js @@ -336,7 +336,7 @@ } function showDialog() { - app.layout.showDialog('share-dialog'); + return app.layout.showDialog('share-dialog'); } // function initDialog() { diff --git a/web/app/assets/javascripts/ui_helper.js b/web/app/assets/javascripts/ui_helper.js index 9bff51a7d..0ed51e8d0 100644 --- a/web/app/assets/javascripts/ui_helper.js +++ b/web/app/assets/javascripts/ui_helper.js @@ -26,25 +26,25 @@ function launchCommentDialog(options) { var commentDialog = new JK.CommentDialog(JK.app, options); commentDialog.initialize(); - commentDialog.showDialog(); + return commentDialog.showDialog(); } function launchShareDialog(entityId, entityType) { var shareDialog = new JK.ShareDialog(JK.app, entityId, entityType); shareDialog.initialize(JK.FacebookHelperInstance); - shareDialog.showDialog(); + return shareDialog.showDialog(); } function launchRsvpSubmitDialog(sessionId) { var rsvpDialog = new JK.RsvpSubmitDialog(JK.app, sessionId); rsvpDialog.initialize(); - rsvpDialog.showDialog(); + return rsvpDialog.showDialog(); } function launchRsvpCancelDialog(sessionId, rsvpRequestId) { var rsvpDialog = new JK.RsvpCancelDialog(JK.app, sessionId, rsvpRequestId); rsvpDialog.initialize(); - rsvpDialog.showDialog(); + return rsvpDialog.showDialog(); } this.addSessionLike = addSessionLike; diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css index 7dba0e458..0fe1feba3 100644 --- a/web/app/assets/stylesheets/client/client.css +++ b/web/app/assets/stylesheets/client/client.css @@ -54,6 +54,7 @@ *= require ./textMessageDialog *= require ./acceptFriendRequestDialog *= require ./launchAppDialog + *= require ./editRecordingDialog *= require ./iconInstrumentSelect *= require ./terms *= require ./createSession diff --git a/web/app/assets/stylesheets/client/editRecordingDialog.css.scss b/web/app/assets/stylesheets/client/editRecordingDialog.css.scss new file mode 100644 index 000000000..5a38282a4 --- /dev/null +++ b/web/app/assets/stylesheets/client/editRecordingDialog.css.scss @@ -0,0 +1,79 @@ +@import "client/common"; + +#edit-recording-dialog { + + min-height:330px; + + input, textarea { + @include border_box_sizing; + } + + .field { + margin-top:20px; + + &:nth-of-type(1) { + margin-top:0; + } + } + + .buttons { + float:right; + clear:both; + margin-top:20px; + } + + label[for="name"] { + + } + input[name="name"] { + margin-top:5px; + width:100%; + } + + label[for="description"] { + + } + textarea[name="description"] { + margin-top:5px; + width:100%; + } + + label[for="genre"] { + display:inline; + float:left; + line-height:26px; + vertical-align:middle; + } + select[name="genre"] { + float:left; + margin-left:5px; + } + + .genre-selector { + float:left; + .dropdown-wrapper { + margin-left:5px; + } + } + + label[for="is_public"] { + display: inline; + float:right; + line-height: 26px; + padding-right: 5px; + vertical-align: middle; + } + select[name="is_public"] { + float:right; + } + div[purpose="is_public"] { + float:right; + .icheckbox_minimal { + float:right; + margin-top:4px; + } + } + + + +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/feed.css.scss b/web/app/assets/stylesheets/client/feed.css.scss index 4dbd60d4f..a69727e00 100644 --- a/web/app/assets/stylesheets/client/feed.css.scss +++ b/web/app/assets/stylesheets/client/feed.css.scss @@ -2,4 +2,9 @@ .recording-current { position:absolute; // solves a problem with duration wrapping--only in firefox } + + .btn-refresh-holder { + float:right; + margin-right:10px; + } } \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/jamkazam.css.scss b/web/app/assets/stylesheets/client/jamkazam.css.scss index e4256229f..16d357c82 100644 --- a/web/app/assets/stylesheets/client/jamkazam.css.scss +++ b/web/app/assets/stylesheets/client/jamkazam.css.scss @@ -328,6 +328,7 @@ input[type="text"], input[type="password"]{ textarea { font-size:15px; + padding:3px; } diff --git a/web/app/assets/stylesheets/client/profile.css.scss b/web/app/assets/stylesheets/client/profile.css.scss index 746f783e3..ff45db852 100644 --- a/web/app/assets/stylesheets/client/profile.css.scss +++ b/web/app/assets/stylesheets/client/profile.css.scss @@ -317,4 +317,60 @@ #btn-add-friend { display:none; +} + +#profile-history { + padding:0 10px 0 20px; + width:100%; + position:relative; + height:100%; + @include border_box_sizing; + + #user-feed-controls { + width:100%; + @include border_box_sizing; + position:relative; + display:none; + } + + .btn-refresh-holder { + left: 95%; // 5 * 19% to right-align 5 user blocks (in conjunction with the margin-left + margin-left: -65px; + position: absolute; + } + + .filter-body { + bottom: 0; + right: 0; + top: 0; + left: 20px; + position: absolute; + width: 95%; + padding-top:0; + margin-top:10px; + height:auto; + } + + .profile-wrapper { + padding: 10px 0; + } + + .feed-entry .feed-details { + margin-right:5px; + } + + .recording-current { + position:absolute; // solves a problem with duration wrapping--only in firefox + } + + .content-body-scroller { + height: 100%; + width: 100%; + position: absolute; + } + + + .no-feed-msg { + text-align:center + } } \ No newline at end of file diff --git a/web/app/assets/stylesheets/web/audioWidgets.css.scss b/web/app/assets/stylesheets/web/audioWidgets.css.scss index da7a933f1..1fcd93f85 100644 --- a/web/app/assets/stylesheets/web/audioWidgets.css.scss +++ b/web/app/assets/stylesheets/web/audioWidgets.css.scss @@ -272,6 +272,20 @@ } } + a.edit-recording-dialog { + font-size:12px; + display:none; + } + + .is_private { + display:none; + font-size:12px; + font-style:italic; + &.enabled { + display:inline; + } + } + .play-count { margin-right:10px; } diff --git a/web/app/controllers/api_claimed_recordings_controller.rb b/web/app/controllers/api_claimed_recordings_controller.rb index 61eaa9de2..04cef8d3f 100644 --- a/web/app/controllers/api_claimed_recordings_controller.rb +++ b/web/app/controllers/api_claimed_recordings_controller.rb @@ -19,30 +19,20 @@ class ApiClaimedRecordingsController < ApiController end def update - if @claimed_recording.user_id != current_user.id raise PermissionError, 'only owner of claimed_recording can update it' end - begin - @claimed_recording.update_fields(current_user, params) - respond_with responder: ApiResponder, :status => 204 - rescue - render :json => { :message => "claimed_recording could not be updated" }, :status => 403 - end + @claimed_recording.update_fields(current_user, params) + respond_with @claimed_recording end def delete if @claimed_recording.user_id != current_user.id raise PermissionError, 'only owner of claimed_recording can update it' end - #begin - #@claimed_recording.discard(current_user) - #render :json => {}, :status => 204 -# respond_with responder: ApiResponder, :status => 204 - #rescue - #render :json => { :message => "claimed_recording could not be deleted" }, :status => 403 - #end + @claimed_recording.discard(current_user) + render :json => {}, :status => 200 end def download diff --git a/web/app/controllers/api_recordings_controller.rb b/web/app/controllers/api_recordings_controller.rb index 478a3aeee..75e3ea6f4 100644 --- a/web/app/controllers/api_recordings_controller.rb +++ b/web/app/controllers/api_recordings_controller.rb @@ -205,6 +205,7 @@ class ApiRecordingsController < ApiController end end + private def parse_filename @recorded_track = RecordedTrack.find_by_recording_id_and_client_track_id!(params[:id], params[:track_id]) diff --git a/web/app/views/api_feeds/show.rabl b/web/app/views/api_feeds/show.rabl index 03a8ad2c5..140828833 100644 --- a/web/app/views/api_feeds/show.rabl +++ b/web/app/views/api_feeds/show.rabl @@ -140,7 +140,7 @@ glue :recording do child(:claimed_recordings => :claimed_recordings) { - attributes :id, :name, :description, :is_public, :genre_id, :has_mix? + attributes :id, :name, :description, :is_public, :genre_id, :has_mix?, :user_id child(:user => :creator) { attributes :id, :first_name, :last_name, :photo_url diff --git a/web/app/views/api_music_sessions/show_history.rabl b/web/app/views/api_music_sessions/show_history.rabl index 1472378cb..216428716 100644 --- a/web/app/views/api_music_sessions/show_history.rabl +++ b/web/app/views/api_music_sessions/show_history.rabl @@ -17,7 +17,7 @@ if !current_user else attributes :id, :music_session_id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat, - :band_id, :user_id, :genre_id, :created_at, :like_count, :comment_count, :scheduled_start, :scheduled_duration, + :band_id, :user_id, :genre_id, :created_at, :like_count, :comment_count, :play_count, :scheduled_start, :scheduled_duration, :language, :recurring_mode, :language_description, :scheduled_start_time, :access_description, :timezone, :timezone_description, :musician_access_description, :fan_access_description, :session_removed_at, :legal_policy, :open_rsvps diff --git a/web/app/views/api_recordings/show.rabl b/web/app/views/api_recordings/show.rabl index 633816ba0..8b75f0384 100644 --- a/web/app/views/api_recordings/show.rabl +++ b/web/app/views/api_recordings/show.rabl @@ -28,7 +28,7 @@ child(:comments => :comments) { child(:claimed_recordings => :claimed_recordings) { - attributes :id, :name, :description, :is_public, :genre_id + attributes :id, :name, :description, :is_public, :genre_id, :has_mix?, :user_id node :share_url do |claimed_recording| unless claimed_recording.share_token.nil? diff --git a/web/app/views/clients/_banner.html.erb b/web/app/views/clients/_banner.html.erb deleted file mode 100644 index 1bcde7cbd..000000000 --- a/web/app/views/clients/_banner.html.erb +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/web/app/views/clients/_banner.html.haml b/web/app/views/clients/_banner.html.haml new file mode 100644 index 000000000..9e53fb0e1 --- /dev/null +++ b/web/app/views/clients/_banner.html.haml @@ -0,0 +1,33 @@ +#banner_overlay.overlay +#banner.dialog-overlay-sm{ 'data-type' => '' } + .content-head + = image_tag("content/icon_alert.png", :height => '24', :width => '24', :class => "content-icon") + %h1 + + .dialog-inner + + %br.end-content{ clear: 'all'} + + .right.buttons + %a.button-orange.close-btn CLOSE + %a.button-orange.yes-btn YES + %a.button-grey.no-btn CANCEL + +%script{type: 'text/template', id: 'template-app-in-read-only-volume'} + .template-app-in-read-only-volume + %p The JamKazam application is running in a read-only volume. This stops the automatic update feature from working, and may cause other issues because it is not a supported configuration. + %p So let's fix it. Don't worry--it's easy to do--please read on. + %p First, here's almost certainly what happened to cause this problem: after JamKazam.dmg was downloaded, it was then double-clicked and a window opened showing the contents of the dmg. The JamKazam application icon was double-clicked inside that opened window. Unfortunately, that isn't OK. + %p Instead, do this to move JamKazam to a good location, and run it from there: + %ol + %li.download-dmg + Download the latest mac installer from the + %a{href:"/downloads"}Downloads + page. + %br + %em (the download will have a filename ending in .dmg) + %li Double-click the downloaded dmg file to open it. + %li In the resulting screen, drag the JamKazam icon to the Applications folder. It will show a progress bar as it copies. + %li Double-click the Applications folder to go into it. + %li If you are still running the JamKazam application, you will need to stop it before executing the last step. + %li Find the JamKazam application in the Applications folder, and double-click the icon to launch it! \ No newline at end of file diff --git a/web/app/views/clients/_edit_recording_dialog.html.haml b/web/app/views/clients/_edit_recording_dialog.html.haml new file mode 100644 index 000000000..b1b944c4d --- /dev/null +++ b/web/app/views/clients/_edit_recording_dialog.html.haml @@ -0,0 +1,24 @@ +.dialog.configure-tracks{ layout: 'dialog', 'layout-id' => 'edit-recording', id: 'edit-recording-dialog'} + .content-head + = image_tag "content/icon_add.png", {:width => 19, :height => 19, :class => 'content-icon' } + %h1 Edit Recording + .dialog-inner + %form + .field + %label{for: 'name'} Recording name: + %input{type: 'text', name: 'name'} + .field + %label{for: 'description'} Description: + %textarea{name: 'description', rows: '4'} + .field.genre-selector + %label{for: 'genre'} Genre: + %select{name:'genre'} + .field{purpose: 'is_public'} + %input{type: 'checkbox', name: 'is_public'} + %label{for: 'is_public'} Public Recording + + .buttons + %a.button-grey.cancel-btn CANCEL + %a.button-orange.delete-btn DELETE + %a.button-orange.save-btn UPDATE + %br{clear: 'all'} \ No newline at end of file diff --git a/web/app/views/clients/_profile.html.erb b/web/app/views/clients/_profile.html.erb index 21f06b84d..b4ad60577 100644 --- a/web/app/views/clients/_profile.html.erb +++ b/web/app/views/clients/_profile.html.erb @@ -88,9 +88,19 @@
+ <%= form_tag('', {:id => 'user-feed-form', :class => 'inner-content'}) do %> + <%= render(:partial => "web_filter", :locals => {:search_type => Search::PARAM_FEED, :id => 'user-feed-controls'}) %> +
-
+
+
+ Next +
No more feed entries
+
+
+ + <% end %>

diff --git a/web/app/views/clients/_web_filter.html.erb b/web/app/views/clients/_web_filter.html.erb index 5beed3237..6baa9cfc7 100644 --- a/web/app/views/clients/_web_filter.html.erb +++ b/web/app/views/clients/_web_filter.html.erb @@ -72,8 +72,8 @@ <% end -%> <% if :feed == filter_label %> -
- REFRESH + <% end %> <% end -%> diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 602b82afb..2ab03c5c5 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -50,6 +50,7 @@ <%= render "account_audio_profile" %> <%= render "account_sessions" %> <%= render "configure_tracks_dialog" %> +<%= render "edit_recording_dialog" %> <%= render "invitationDialog" %> <%= render "inviteMusicians" %> <%= render "hoverBand" %> @@ -129,6 +130,9 @@ if (this.didInitAfterConnect) return; this.didInitAfterConnect = true + // This is a helper class with a singleton. No need to instantiate. + JK.GenreSelectorHelper.initialize(JK.app); + var recordingManager = new JK.RecordingManager(); var facebookHelper = new JK.FacebookHelper(JK.app); @@ -156,6 +160,8 @@ var launchAppDialog = new JK.LaunchAppDialog(JK.app); launchAppDialog.initialize(); + var editRecordingDialog = new JK.EditRecordingDialog(JK.app); + editRecordingDialog.initialize(); var userDropdown = new JK.UserDropdown(JK.app); JK.UserDropdown = userDropdown; @@ -202,9 +208,6 @@ var searchResultScreen = new JK.SearchResultScreen(JK.app); searchResultScreen.initialize(); - // This is a helper class with a singleton. No need to instantiate. - JK.GenreSelectorHelper.initialize(); - var inviteMusiciansUtil1 = new JK.InviteMusiciansUtil(JK.app); inviteMusiciansUtil1.initialize(friendSelectorDialog); // var createSessionScreen = new JK.CreateSessionScreen(JK.app); diff --git a/web/app/views/users/_feed_recording_ajax.html.haml b/web/app/views/users/_feed_recording_ajax.html.haml index 25198edeb..6e3eb140d 100644 --- a/web/app/views/users/_feed_recording_ajax.html.haml +++ b/web/app/views/users/_feed_recording_ajax.html.haml @@ -1,5 +1,5 @@ %script{type: 'text/template', id: 'template-feed-recording'} - .feed-entry.recording-entry{:'data-claimed-recording-id' => '{{data.candidate_claimed_recording.id}}' } + .feed-entry.recording-entry{:'data-claimed-recording-id' => '{{data.candidate_claimed_recording.id}}', :'data-recording-id' => '{{data.feed_item.id}}' } / avatar .avatar-small.ib %a{:hoveraction => "{{data.feed_item.helpers.artist_hoveraction}}", :profileaction => "{{data.feed_item.helpers.artist_hoveraction}}", :"{{data.feed_item.helpers.artist_datakey}}" => "{{data.feed_item.helpers.artist_id}}"} @@ -8,6 +8,7 @@ .left.ml20.w15 .title{hoveraction: 'recording', :'recording-id' => '{{data.candidate_claimed_recording.id}}' } %a{:href => "/recordings/{{data.candidate_claimed_recording.id}}", :rel => "external"} RECORDING + %a.edit-recording-dialog{href: "#"} (edit) .artist %a.artist{:hoveraction => '{{data.feed_item.helpers.artist_hoveraction}}', :profileaction => "{{data.feed_item.helpers.artist_hoveraction}}", :'{{data.feed_item.helpers.artist_datakey}}' => '{{data.feed_item.helpers.artist_id}}'} = '{{data.feed_item.helpers.artist_name}}' @@ -16,7 +17,8 @@ / name and description .left.ml20.w30 .name.dotdotdot - = '{{data.feed_item.helpers.name}}' + %span.name-text {{data.feed_item.helpers.name}} + %span.is_private (private) .description.dotdotdot = '{{data.feed_item.helpers.description}}' / timeline and controls @@ -54,7 +56,7 @@ 0:00 / end recording play controls / genre and social - .left.small= '{{data.feed_item.helpers.genre}}' + .left.small.genre= '{{data.feed_item.helpers.genre}}' .right.small.feed-details %a{title: 'Share', class: 'btn-share'} = image_tag 'content/icon_share.png', :height => "12", :width => "7" diff --git a/web/spec/features/feed_spec.rb b/web/spec/features/feed_spec.rb index a177c63b5..4e3bff388 100644 --- a/web/spec/features/feed_spec.rb +++ b/web/spec/features/feed_spec.rb @@ -56,7 +56,7 @@ describe "Feed", :js => true, :type => :feature, :capybara_feature => true do find('div.comment-text', text: comment) find('#dialog-close-button', '[layout-id="comment-dialog"]').trigger(:click) - find('#btn-refresh-feed').trigger(:click) + find('[layout-id="feed"] .btn-refresh-entries').trigger(:click) find('span.comments').should have_content('1') # Likes @@ -142,7 +142,7 @@ describe "Feed", :js => true, :type => :feature, :capybara_feature => true do find('div.comment-text', text: comment) find('#dialog-close-button', '[layout-id="comment-dialog"]').trigger(:click) - find('#btn-refresh-feed').trigger(:click) + find('[layout-id="feed"] .btn-refresh-entries').trigger(:click) find('span.comments').should have_content('1') # Likes @@ -173,12 +173,6 @@ describe "Feed", :js => true, :type => :feature, :capybara_feature => true do find("#user-profile h2[id=profile-username]", text: user.name) end - # it "should render play widget" do - - # it " and allow recording playback" do - # end - # end - end end \ No newline at end of file diff --git a/web/spec/features/profile_history_spec.rb b/web/spec/features/profile_history_spec.rb new file mode 100644 index 000000000..d9662d2c9 --- /dev/null +++ b/web/spec/features/profile_history_spec.rb @@ -0,0 +1,196 @@ +require 'spec_helper' + +describe "Profile History", :js => true, :type => :feature, :capybara_feature => true do + + subject { page } + + let(:user) { FactoryGirl.create(:user) } + + before do + MusicSession.delete_all + Recording.delete_all + sign_in_poltergeist user + stub_const("APP_CONFIG", web_config) + end + + it "loads empty history" do + nav_profile_history(user) + + find('.no-feed-msg', text: 'This user has no history.') + end + + + # the same feedHelper instance is used to show any user's feed. so we need to make sure bouncing back and forth + # between feeds is safe + it "between users" do + create_session(creator: user) + formal_leave_by(user) + user2 = FactoryGirl.create(:user) + + nav_profile_history(user) + find('.feed-entry.music-session-history-entry') + visit Nav.feed # to get around bug where you can't cause a screen to update if it's the same screen, different params + nav_profile_history(user2) + should_not have_selector('.feed-entry.music-session-history-entry') + find('.no-feed-msg', text: 'This user has no history.') + end + + describe "sessions" do + + before(:each) do + create_session(creator: user) + formal_leave_by(user) + end + + it "should render stats" do + nav_profile_history(user) + + # initial stats + find('span.plays').should have_content('0') + find('span.comments').should have_content('0') + find('span.likes').should have_content('0') + + # Comments + find('a.btn-comment').trigger(:click) + comment = 'this sounds great' + fill_in "txtComment", with: comment + find('#btn-add-comment').trigger(:click) + find('div.comment-text', text: comment) + find('#dialog-close-button', '[layout-id="comment-dialog"]').trigger(:click) + find('span.comments').should have_content('1') + + # Likes + find('a.btn-like').trigger(:click) + find('span.likes').should have_content('1') + end + + it "should render details" do + nav_profile_history(user) + find('.feed-details a.details').trigger(:click) + + # confirm user avatar exists + find("a.avatar-tiny[user-id=\"#{user.id}\"][hoveraction=\"musician\"] img") + + # confirm user name exists + find("a.musician-name[user-id=\"#{user.id}\"][hoveraction=\"musician\"]", text: user.name) + + # confirm instrument icons exist + find("img[instrument-id=\"electric guitar\"]") + + # confirm hover bubbles show + find("a.avatar-tiny[user-id=\"#{user.id}\"][hoveraction=\"musician\"]").hover_intent + + # confirm navigate to user profile page + find(".avatar-tiny[user-id=\"#{user.id}\"][hoveraction=\"musician\"]").trigger(:click) + find("#user-profile h2[id=profile-username]", text: user.name) + end + + end + + describe "recordings" do + + before(:each) do + start_recording_with(user) + stop_recording + claim_recording("my recording", "my recording description") + formal_leave_by(user) + MusicSession.delete_all + end + + # it "should render avatar" do + # it " and link to profile" do + # end + + # it " and render artist hover bubble" do + # end + # end + + # it "should render description" do + # it " and link to recording landing" do + # end + + # it " and render recording hover bubble" do + # end + # end + + # it "should render artist name" do + # it " and link to profile" do + # end + + # it " and render artist hover bubble" + # end + + it "should render stats" do + nav_profile_history(user) + + # initial stats + find('span.plays').should have_content('0') + find('span.comments').should have_content('0') + find('span.likes').should have_content('0') + + # ensure Share icon exists + find('a.btn-share') + + # Comments + find('a.btn-comment').trigger(:click) + comment = 'this sounds great' + fill_in "txtComment", with: comment + find('#btn-add-comment').trigger(:click) + find('div.comment-text', text: comment) + find('#dialog-close-button', '[layout-id="comment-dialog"]').trigger(:click) + + find('span.comments').should have_content('1') + + # Likes + find('a.btn-like').trigger(:click) + find('span.likes').should have_content('1') + end + + it "should render details" do + nav_profile_history(user) + + find('.feed-details a.details').trigger(:click) + + # confirm user avatar exists + find("a.avatar-tiny[user-id=\"#{user.id}\"][hoveraction=\"musician\"] img") + + # confirm user name exists + find("a.musician-name[user-id=\"#{user.id}\"][hoveraction=\"musician\"]", text: user.name) + + # confirm instrument icons exist + find("img[instrument-id=\"electric guitar\"]") + + + # confirm hover bubbles show + find("a.avatar-tiny[user-id=\"#{user.id}\"][hoveraction=\"musician\"]").hover_intent + + # confirm navigate to user profile page + find(".avatar-tiny[user-id=\"#{user.id}\"][hoveraction=\"musician\"]").trigger(:click) + find("#user-profile h2[id=profile-username]", text: user.name) + end + + it "should allow edit and delete" do + nav_profile_history(user) + + # edit it's name + find('.edit-recording-dialog').trigger(:click) + find('h1', text: 'Edit Recording') + fill_in 'name', with: 'some crazy name' + find('.save-btn').trigger(:click) + should_not have_selector('h1', text: 'Edit Recording') + find('.name-text', text:'some crazy name') + + # now delete it + find('.edit-recording-dialog').trigger(:click) + find('h1', text: 'Edit Recording') + find('.delete-btn').trigger(:click) + # confirm... + find('.yes-btn').trigger(:click) + should_not have_selector('h1', text: 'Edit Recording') + should_not have_selector('.feed-entry.recording-entry') + + ClaimedRecording.first.discarded.should == true + end + + end +end diff --git a/web/spec/support/app_config.rb b/web/spec/support/app_config.rb index 5f124e9a9..fcbc05edf 100644 --- a/web/spec/support/app_config.rb +++ b/web/spec/support/app_config.rb @@ -17,6 +17,35 @@ def web_config def external_root_url "#{external_protocol}#{external_hostname}#{(external_port == 80 || external_port == 443) ? '' : ':' + external_port.to_s}" end + + + def aws_bucket + JAMKAZAM_TESTING_BUCKET + end + + def aws_access_key_id + 'AKIAJESQY24TOT542UHQ' + end + + def aws_secret_access_key + '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' + end + + def aws_region + 'us-east-1' + end + + def aws_bucket_public + 'jamkazam-testing-public' + end + + def aws_cache + '315576000' + end + + def max_audio_downloads + 10 + end end klass.new end diff --git a/web/spec/support/utilities.rb b/web/spec/support/utilities.rb index 081cca030..7bc112ad0 100644 --- a/web/spec/support/utilities.rb +++ b/web/spec/support/utilities.rb @@ -581,3 +581,8 @@ def garbage length length.times { output << special_characters.sample } output.slice(0, length) end + +def nav_profile_history(user) + visit Nav.profile(user) + find('#profile-history-link').trigger(:click) +end \ No newline at end of file