diff --git a/Gemfile b/Gemfile index eecfddd925508b86b4f44682696ebc96b0a881c1..6efc0df6cebd1bd689e72fa6079301173bfdd284 100644 --- a/Gemfile +++ b/Gemfile @@ -17,6 +17,7 @@ gem 'rack-cors', '~> 0.2.4', :require => 'rack/cors' gem 'devise', '1.5.3' gem 'jwt' gem 'oauth2-provider', '0.0.19' +gem 'remotipart', '~> 1.0' gem 'omniauth', '1.0.1' gem 'omniauth-facebook' diff --git a/Gemfile.lock b/Gemfile.lock index e97cd5e605a9468a0c574d36f5b0793b89753d49..d3e313444b73d44691b7a60b0feca57eae00569c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -162,7 +162,7 @@ GEM fixture_builder (0.3.1) activerecord (>= 2) activesupport (>= 2) - fog (1.2.0) + fog (1.3.0) builder excon (~> 0.12.0) formatador (~> 0.2.0) @@ -341,6 +341,7 @@ GEM redis (2.2.2) redis-namespace (1.0.3) redis (< 3.0.0) + remotipart (1.0.2) resque (1.20.0) multi_json (~> 1.0) redis-namespace (~> 1.0.2) @@ -513,6 +514,7 @@ DEPENDENCIES rails-i18n rails_autolink redcarpet (= 2.0.1) + remotipart (~> 1.0) resque (= 1.20.0) resque-timeout (= 1.0.0) rest-client (= 1.6.7) diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index c2d1978da9acb6a8b906a6a1156f7dbe5c77d51c..a419bccb0bc0b93e8a4948c1b87849c94a32b584 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -41,55 +41,19 @@ class PhotosController < ApplicationController end def create - begin - raise unless params[:photo][:aspect_ids] - - if params[:photo][:aspect_ids] == "all" - params[:photo][:aspect_ids] = current_user.aspects.collect{|x| x.id} - elsif params[:photo][:aspect_ids].is_a?(Hash) - params[:photo][:aspect_ids] = params[:photo][:aspect_ids].values - end - - params[:photo][:user_file] = file_handler(params) - - @photo = current_user.build_post(:photo, params[:photo]) - - if @photo.save - - aspects = current_user.aspects_from_ids(params[:photo][:aspect_ids]) - - unless @photo.pending - current_user.add_to_streams(@photo, aspects) - current_user.dispatch_post(@photo, :to => params[:photo][:aspect_ids]) - end - - if params[:photo][:set_profile_photo] - profile_params = {:image_url => @photo.url(:thumb_large), - :image_url_medium => @photo.url(:thumb_medium), - :image_url_small => @photo.url(:thumb_small)} - current_user.update_profile(profile_params) - end - - respond_to do |format| - format.json{ render(:layout => false , :json => {"success" => true, "data" => @photo}.to_json )} - format.html{ render(:layout => false , :json => {"success" => true, "data" => @photo}.to_json )} + rescuing_photo_errors do + if remotipart_submitted? + @photo = current_user.build_post(:photo, params[:photo]) + if @photo.save + respond_to do |format| + format.json { render :json => {"success" => true, "data" => @photo.as_api_response(:backbone)} } + end + else + respond_with @photo, :location => photos_path, :error => message end else - respond_with @photo, :location => photos_path, :error => message + legacy_create end - - rescue TypeError - message = I18n.t 'photos.create.type_error' - respond_with @photo, :location => photos_path, :error => message - - rescue CarrierWave::IntegrityError - message = I18n.t 'photos.create.integrity_error' - respond_with @photo, :location => photos_path, :error => message - - rescue RuntimeError => e - message = I18n.t 'photos.create.runtime_error' - respond_with @photo, :location => photos_path, :error => message - raise e end end @@ -200,4 +164,57 @@ class PhotosController < ApplicationController file end end + + def legacy_create + if params[:photo][:aspect_ids] == "all" + params[:photo][:aspect_ids] = current_user.aspects.collect { |x| x.id } + elsif params[:photo][:aspect_ids].is_a?(Hash) + params[:photo][:aspect_ids] = params[:photo][:aspect_ids].values + end + + params[:photo][:user_file] = file_handler(params) + + @photo = current_user.build_post(:photo, params[:photo]) + + if @photo.save + aspects = current_user.aspects_from_ids(params[:photo][:aspect_ids]) + + unless @photo.pending + current_user.add_to_streams(@photo, aspects) + current_user.dispatch_post(@photo, :to => params[:photo][:aspect_ids]) + end + + if params[:photo][:set_profile_photo] + profile_params = {:image_url => @photo.url(:thumb_large), + :image_url_medium => @photo.url(:thumb_medium), + :image_url_small => @photo.url(:thumb_small)} + current_user.update_profile(profile_params) + end + + respond_to do |format| + format.json{ render(:layout => false , :json => {"success" => true, "data" => @photo}.to_json )} + format.html{ render(:layout => false , :json => {"success" => true, "data" => @photo}.to_json )} + end + else + respond_with @photo, :location => photos_path, :error => message + end + end + + def rescuing_photo_errors + begin + yield + rescue TypeError + message = I18n.t 'photos.create.type_error' + respond_with @photo, :location => photos_path, :error => message + + rescue CarrierWave::IntegrityError + message = I18n.t 'photos.create.integrity_error' + respond_with @photo, :location => photos_path, :error => message + + rescue RuntimeError => e + message = I18n.t 'photos.create.runtime_error' + respond_with @photo, :location => photos_path, :error => message + raise e + end + end end diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 667eb51a49125d20f789e59612ce22f90b13955b..128c7cc4f277878b9ecf41e5f38191391abb0444 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -8,11 +8,18 @@ class PostsController < ApplicationController before_filter :authenticate_user!, :except => :show before_filter :set_format_if_malformed_from_status_net, :only => :show + layout 'post' + respond_to :html, :mobile, :json, :xml + def new + redirect_to "/stream" and return unless FeatureFlags.new_publisher + render :text => "", :layout => true + end + def show key = params[:id].to_s.length <= 8 ? :id : :guid @@ -34,7 +41,7 @@ class PostsController < ApplicationController format.xml{ render :xml => @post.to_diaspora_xml } format.mobile{render 'posts/show.mobile.haml'} format.json{ render :json => PostPresenter.new(@post, current_user).to_json } - format.any{render 'posts/show.html.haml', :layout => 'layouts/post'} + format.any{render 'posts/show.html.haml'} end else diff --git a/app/controllers/status_messages_controller.rb b/app/controllers/status_messages_controller.rb index 5953993a925886268f86e03d565d8ec4bdac76da..b6764b890a61c456607587c7b1635797e44c5878 100644 --- a/app/controllers/status_messages_controller.rb +++ b/app/controllers/status_messages_controller.rb @@ -53,7 +53,8 @@ class StatusMessagesController < ApplicationController receiving_services = Service.titles(services) current_user.dispatch_post(@status_message, :url => short_post_url(@status_message.guid), :service_types => receiving_services) - + + #this is done implicitly, somewhere else, apparently, says max. :'( # @status_message.photos.each do |photo| # current_user.dispatch_post(photo) # end diff --git a/app/models/feature_flags.rb b/app/models/feature_flags.rb new file mode 100644 index 0000000000000000000000000000000000000000..ef0ae3fc5c80149fae90acccbd7c5d607241e023 --- /dev/null +++ b/app/models/feature_flags.rb @@ -0,0 +1,5 @@ +module FeatureFlags + def self.new_publisher + !(Rails.env.production? || Rails.env.staging?) + end +end \ No newline at end of file diff --git a/app/models/photo.rb b/app/models/photo.rb index fa76787c51b4f4363a603804a5c3a77afe4a7710..0f3f4aa929991a5a88981b7f2ceeb41b93f53fab 100644 --- a/app/models/photo.rb +++ b/app/models/photo.rb @@ -9,8 +9,6 @@ class Photo < ActiveRecord::Base include Diaspora::Commentable include Diaspora::Shareable - - # NOTE API V1 to be extracted acts_as_api api_accessible :backbone do |t| diff --git a/app/models/service.rb b/app/models/service.rb index 9eab8442a7f90da5a432a12797079a80c71bc72d..8f0ed9d1c37f9bf5e1b67fa80a55cf23afe0b692 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -14,6 +14,7 @@ class Service < ActiveRecord::Base end def public_message(post, length, url = "") + Rails.logger.info("Posting out to #{self.class}") url = "" if post.respond_to?(:photos) && post.photos.count == 0 space_for_url = url.blank? ? 0 : (url.length + 1) truncated = truncate(post.text(:plain_text => true), :length => (length - space_for_url)) diff --git a/app/models/status_message.rb b/app/models/status_message.rb index 289150a87458bb6cc0de419853fd9e93de8d8b60..aad453ac1f1fd71bba7ef5dc27da11f2033a7777 100644 --- a/app/models/status_message.rb +++ b/app/models/status_message.rb @@ -21,7 +21,7 @@ class StatusMessage < Post # therefore, we put the validation in a before_destory callback instead of a validation before_destroy :presence_of_content - attr_accessible :text, :provider_display_name + attr_accessible :text, :provider_display_name, :frame_name attr_accessor :oembed_url after_create :create_mentions diff --git a/app/presenters/aspect_presenter.rb b/app/presenters/aspect_presenter.rb new file mode 100644 index 0000000000000000000000000000000000000000..881be4b3a100f50c342a6f5392f89522562488d9 --- /dev/null +++ b/app/presenters/aspect_presenter.rb @@ -0,0 +1,15 @@ +class AspectPresenter < BasePresenter + def initialize(aspect) + @aspect = aspect + end + + def as_json + { :id => @aspect.id, + :name => @aspect.name, + } + end + + def to_json(options = {}) + as_json.to_json(options) + end +end \ No newline at end of file diff --git a/app/presenters/base_presenter.rb b/app/presenters/base_presenter.rb new file mode 100644 index 0000000000000000000000000000000000000000..8f83962e9a185cb9cf10889167da204b2b4c8ac2 --- /dev/null +++ b/app/presenters/base_presenter.rb @@ -0,0 +1,5 @@ +class BasePresenter + def self.as_collection(collection) + collection.map{|object| self.new(object).as_json} + end +end diff --git a/app/presenters/post_presenter.rb b/app/presenters/post_presenter.rb index f8e1febf54a2e6c4bdb6e7afbf02191be6d13218..0f798494744f2936201bf105a255cc9e5dc335b5 100644 --- a/app/presenters/post_presenter.rb +++ b/app/presenters/post_presenter.rb @@ -23,7 +23,7 @@ class PostPresenter :reshares => self.reshares, :comments => self.comments, :participations => self.participations, - :templateName => template_name, + :frame_name => self.post.frame_name || template_name, :title => title }) end diff --git a/app/presenters/service_presenter.rb b/app/presenters/service_presenter.rb new file mode 100644 index 0000000000000000000000000000000000000000..f44ad8eb7903c5f605532bca16f91055b64fc54d --- /dev/null +++ b/app/presenters/service_presenter.rb @@ -0,0 +1,15 @@ +class ServicePresenter < BasePresenter + def initialize(service) + @service = service + end + + def as_json + { + :provider => @service.provider + } + end + + def to_json(options = {}) + as_json.to_json(options) + end +end \ No newline at end of file diff --git a/app/presenters/user_presenter.rb b/app/presenters/user_presenter.rb index 38436efd5a22403f2d6cb020fdea4f13f3c26ace..8fd5af0a191862645f9862506c5f7c401d17c4cb 100644 --- a/app/presenters/user_presenter.rb +++ b/app/presenters/user_presenter.rb @@ -1,6 +1,6 @@ class UserPresenter attr_accessor :user - + def initialize(user) self.user = user end @@ -9,15 +9,23 @@ class UserPresenter self.user.person.as_api_response(:backbone).update( { :notifications_count => notifications_count, :unread_messages_count => unread_messages_count, - :admin => admin + :admin => admin, + :aspects => aspects, + :services => services } ).to_json(options) end - protected + def services + ServicePresenter.as_collection(user.services) + end + + def aspects + AspectPresenter.as_collection(user.aspects) + end def notifications_count - @notification_count ||= user.unread_notifications.count + @notification_count ||= user.unread_notifications.count end def unread_messages_count @@ -27,4 +35,4 @@ class UserPresenter def admin user.admin? end -end \ No newline at end of file +end diff --git a/app/views/layouts/post.html.haml b/app/views/layouts/post.html.haml index bc10e192a3b9cdcf2a425fb59708ec961c87d768..b2a2e4d0a043c769cbd1aad4df53784fe43de078 100644 --- a/app/views/layouts/post.html.haml +++ b/app/views/layouts/post.html.haml @@ -50,5 +50,6 @@ %body = flash_messages + #container = yield diff --git a/app/views/photos/_new_photo.haml b/app/views/photos/_new_photo.haml index 4da494f18861e0c3a665b7c713bf02794a0c5df7..95fa60e27b5fa8a9308ee2ab80df1d41ccf6c6b7 100644 --- a/app/views/photos/_new_photo.haml +++ b/app/views/photos/_new_photo.haml @@ -83,4 +83,4 @@ }); } - createUploader(); + createUploader(); \ No newline at end of file diff --git a/app/views/posts/show.html.haml b/app/views/posts/show.html.haml index ad90ddc2ee1a49522bf9caa4dacd7c899506b433..5720b3364cf69a6165f2e541f0698630f39c4a2f 100644 --- a/app/views/posts/show.html.haml +++ b/app/views/posts/show.html.haml @@ -4,5 +4,3 @@ - content_for :page_title do = post_page_title @post - -#container diff --git a/config/assets.yml b/config/assets.yml index e2a445ec6a71440a32aaff5ac398643702e98bca..e7086b92b16730b5c9a8c798752f43fed64f82cc 100644 --- a/config/assets.yml +++ b/config/assets.yml @@ -25,12 +25,15 @@ javascripts: - public/javascripts/vendor/timeago.js - public/javascripts/vendor/facebox.js - public/javascripts/vendor/underscore.js + - public/javascripts/vendor/jquery.iframe-transport.js + - public/javascripts/vendor/jquery.remotipart.js - public/javascripts/vendor/jquery.events.input.js - public/javascripts/vendor/jquery.elastic.js - public/javascripts/vendor/jquery.mentionsInput.js - public/javascripts/vendor/jquery.idle-timer.js - public/javascripts/jquery.infinitescroll-custom.js - public/javascripts/jquery.autocomplete-custom.js + - public/javascripts/vendor/jquery.textchange.min.js - public/javascripts/keycodes.js - public/javascripts/fileuploader-custom.js @@ -43,6 +46,7 @@ javascripts: - public/javascripts/app/helpers/* - public/javascripts/app/router.js - public/javascripts/app/views.js + - public/javascripts/app/forms.js - public/javascripts/app/models/post.js - public/javascripts/app/models/* - public/javascripts/app/pages/* @@ -51,6 +55,7 @@ javascripts: - public/javascripts/app/views/content_view.js - public/javascripts/app/views/*.js - public/javascripts/app/views/**/*.js + - public/javascripts/app/forms/*.js - public/javascripts/diaspora.js - public/javascripts/helpers/*.js @@ -71,6 +76,7 @@ javascripts: - public/javascripts/vendor/bootstrap/bootstrap-twipsy.js - public/javascripts/vendor/bootstrap/bootstrap-popover.js + - public/javascripts/vendor/bootstrap/bootstrap-dropdown.js login: - public/javascripts/login.js @@ -90,7 +96,6 @@ javascripts: - public/javascripts/friend-finder.js home: - public/javascripts/publisher.js - - public/javascripts/vendor/jquery.textchange.min.js - public/javascripts/aspect-edit-pane.js - public/javascripts/fileuploader-custom.js people: diff --git a/config/routes.rb b/config/routes.rb index 59fe1a02733216baa503055287eba984509852de..d6063f3d6660fc3f526c0c700fb48e60d717617b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,11 +8,14 @@ Diaspora::Application.routes.draw do resources :status_messages, :only => [:new, :create] - resources :posts, :only => [:show, :destroy] do + resources :posts, :only => [:show, :new, :destroy] do resources :likes, :only => [:create, :destroy, :index] resources :participations, :only => [:create, :destroy, :index] resources :comments, :only => [:new, :create, :destroy, :index] end + + match "/framer" => redirect("/posts/new") + get 'p/:id' => 'posts#show', :as => 'short_post' # roll up likes into a nested resource above resources :comments, :only => [:create, :destroy] do @@ -32,7 +35,6 @@ Diaspora::Application.routes.draw do get "commented" => "streams#commented", :as => "commented_stream" get "aspects" => "streams#aspects", :as => "aspects_stream" - resources :aspects do put :toggle_contact_visibility end diff --git a/db/migrate/20120322223517_add_template_name_to_posts.rb b/db/migrate/20120322223517_add_template_name_to_posts.rb new file mode 100644 index 0000000000000000000000000000000000000000..cedd5951c00ed98bd4bacca148908850466f62ee --- /dev/null +++ b/db/migrate/20120322223517_add_template_name_to_posts.rb @@ -0,0 +1,5 @@ +class AddTemplateNameToPosts < ActiveRecord::Migration + def change # thanks josh susser + add_column :posts, :frame_name, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index f662bf314c9807b39e486cb9b5cf6b496fe0ea7a..1f2bfd100e2ca3c7f01d4e1dd980d916bdb1c70b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20120301143226) do +ActiveRecord::Schema.define(:version => 20120322223517) do create_table "account_deletions", :force => true do |t| t.string "diaspora_handle" @@ -334,6 +334,7 @@ ActiveRecord::Schema.define(:version => 20120301143226) do t.integer "o_embed_cache_id" t.integer "reshares_count", :default => 0 t.datetime "interacted_at" + t.string "frame_name" end add_index "posts", ["author_id", "root_guid"], :name => "index_posts_on_author_id_and_root_guid", :unique => true diff --git a/features/notifications.feature b/features/notifications.feature index 78871ea5712ba4786f366b02ea9390cd06ec25b5..fe91d5817970dad1b6504bd2aba3a424ce953669 100644 --- a/features/notifications.feature +++ b/features/notifications.feature @@ -64,13 +64,10 @@ Feature: Notifications And I focus the comment field And I fill in "text" with "great post!" And I press "Comment" - And I wait for the ajax to finish And I go to the destroy user session page When I sign in as "alice@alice.alice" And I follow "Notifications" in the header - And I wait for the ajax to finish Then the notification dropdown should be visible - And I wait for the ajax to finish Then I should see "commented on your post" And I should have 1 email delivery diff --git a/features/step_definitions/stream_steps.rb b/features/step_definitions/stream_steps.rb index d508ebc58ccd9d773a6c5fec4100a95ed5a4259e..ec173268077e04636cf7d53dda8be14ddbc51264 100644 --- a/features/step_definitions/stream_steps.rb +++ b/features/step_definitions/stream_steps.rb @@ -16,4 +16,10 @@ end Then /^I should have (\d+) nsfw posts$/ do |num_posts| all(".nsfw-shield").size.should == num_posts.to_i +end + +When /^I click the show page link for "([^"]*)"$/ do |post_text| + within(find_post_by_text(post_text)) do + find("time").click + end end \ No newline at end of file diff --git a/features/step_definitions/trumpeter_steps.rb b/features/step_definitions/trumpeter_steps.rb new file mode 100644 index 0000000000000000000000000000000000000000..56d58bc598681b2d28444d538e4895536188cbcc --- /dev/null +++ b/features/step_definitions/trumpeter_steps.rb @@ -0,0 +1,120 @@ +def fill_in_autocomplete(selector, value) + pending #make me work if yr board, investigate send_keys + page.execute_script %Q{$('#{selector}').val('#{value}').keyup()} +end + +def aspects_dropdown + find(".dropdown-toggle") +end + +def select_from_dropdown(option_text, dropdown) + dropdown.click + within ".dropdown-menu" do + link = find("a:contains('#{option_text}')") + link.should be_visible + link.click + end + #assert dropdown text is link +end + +def go_to_framer + click_button "Next" +end + +def finalize_frame + click_button "done" +end + +def assert_post_renders_with(template_name) + find(".post")["data-template"].should == template_name.downcase +end + +def find_image_by_filename(filename) + find("img[src='#{@image_sources[filename]}']") +end + +def store_image_filename(file_name) + @image_sources ||= {} + @image_sources[file_name] = all(".photos img").last["src"] + @image_sources[file_name].should be_present +end + +def upload_photo(file_name) + orig_photo_count = all(".photos img").size + + within ".new_photo" do + attach_file "photo[user_file]", Rails.root.join("spec", "fixtures", file_name) + wait_until { all(".photos img").size == orig_photo_count + 1 } + end + + store_image_filename(file_name) +end + +When /^I trumpet$/ do + visit new_post_path +end + +When /^I write "([^"]*)"$/ do |text| + fill_in 'text', :with => text +end + +Then /I mention "([^"]*)"$/ do |text| + fill_in_autocomplete('textarea.text', '@a') + sleep(5) + find("li.active").click +end + +When /^I select "([^"]*)" in my aspects dropdown$/ do |title| + within ".aspect_selector" do + select_from_dropdown(title, aspects_dropdown) + end +end + +Then /^"([^"]*)" should be a (limited|public) post in my stream$/ do |post_text, scope| + find_post_by_text(post_text).find(".post_scope").text.should =~ /#{scope}/i +end + +When /^I upload a fixture picture with filename "([^"]*)"$/ do |file_name| + upload_photo(file_name) +end + +Then /^"([^"]*)" should have the "([^"]*)" picture$/ do |post_text, file_name| + within find_post_by_text(post_text) do + find_image_by_filename(file_name).should be_present + end +end + +When /^I go through the default composer$/ do + go_to_framer + finalize_frame +end + +When /^I start the framing process$/ do + go_to_framer +end + +When /^I finalize my frame$/ do + finalize_frame +end + +Then /^"([^"]*)" should have (\d+) pictures$/ do |post_text, number_of_pictures| + find_post_by_text(post_text).all(".photo_attachments img").size.should == number_of_pictures.to_i +end + +Then /^I should see "([^"]*)" in the framer preview$/ do |post_text| + within(find(".post")) { page.should have_content(post_text) } +end + +When /^I select the mood "([^"]*)"$/ do |template_name| + select template_name, :from => 'template' +end + +Then /^the post's mood should (?:still |)be "([^"]*)"$/ do |template_name| + assert_post_renders_with(template_name) +end + +When /^"([^"]*)" should be in the post's picture viewer$/ do |file_name| + within(".photo_viewer") do + find_image_by_filename(file_name).should be_present + end +end \ No newline at end of file diff --git a/features/trumpeter.feature b/features/trumpeter.feature new file mode 100644 index 0000000000000000000000000000000000000000..cbeb8a45cf2965cce54cd7cda0d0e0328d72e948 --- /dev/null +++ b/features/trumpeter.feature @@ -0,0 +1,59 @@ +@javascript +Feature: Creating a new post + Background: + Given a user with username "bob" + And I sign in as "bob@bob.bob" + And I trumpet + + Scenario: Posting a public message with a photo + And I write "I love RMS" + When I select "Public" in my aspects dropdown + And I upload a fixture picture with filename "button.gif" + When I go through the default composer + When I go to "/stream" + Then I should see "I love RMS" as the first post in my stream + And "I love RMS" should be a public post in my stream + Then "I love RMS" should have the "button.gif" picture + + Scenario: Posting to Aspects + And I write "This is super skrunkle" + When I select "All Aspects" in my aspects dropdown + And I go through the default composer + When I go to "/stream" + Then I should see "This is super skrunkle" as the first post in my stream + Then "This is super skrunkle" should be a limited post in my stream + + Scenario: Mention a contact + Given a user named "Alice Smith" with email "alice@alice.alice" + And a user with email "bob@bob.bob" is connected with "alice@alice.alice" + And I mention "alice@alice.alice" + And I go through the default composer + And I go to "/stream" + Then I follow "Alice Smith" + + Scenario: Uploading multiple photos + When I write "check out these pictures" + And I upload a fixture picture with filename "button.gif" + And I upload a fixture picture with filename "button.gif" + And I go through the default composer + And I go to "/stream" + Then "check out these pictures" should have 2 pictures + + Scenario: Framing your frame + When I write "This is hella customized" + And I upload a fixture picture with filename "button.gif" + + And I start the framing process + Then I should see "This is hella customized" in the framer preview + # Then the default mood for the post should be "Wallpaper" + # And I should see the image "button.gif" background + When I select the mood "Day" + Then the post's mood should be "Day" + And "button.gif" should be in the post's picture viewer + And I should see "This is hella customized" in the framer preview + + When I finalize my frame + And I go to "/stream" + Then "This is hella customized" should be post 1 + And I click the show page link for "This is hella customized" + And the post's mood should still be "Day" diff --git a/public/javascripts/app/app.js b/public/javascripts/app/app.js index 3f019e0f1a3f7e5ebaf04d7d75fef3b8305900c1..962c7a181bb5dce3e0b2265002f6e880b7a61bbc 100644 --- a/public/javascripts/app/app.js +++ b/public/javascripts/app/app.js @@ -4,6 +4,7 @@ var app = { helpers: {}, views: {}, pages: {}, + forms: {}, user: function(userAttrs) { if(userAttrs) { return this._user = new app.models.User(userAttrs) } diff --git a/public/javascripts/app/forms.js b/public/javascripts/app/forms.js new file mode 100644 index 0000000000000000000000000000000000000000..a9f3fe10546e626abe9571b758829b1274740b06 --- /dev/null +++ b/public/javascripts/app/forms.js @@ -0,0 +1,14 @@ +app.forms.Base = app.views.Base.extend({ + formSelector : "form", + + initialize : function() { + this.setupFormEvents() + }, + + setupFormEvents : function(){ + this.events = {} + this.events['submit ' + this.formSelector] = 'setModelAttributes'; + this.delegateEvents(); + }, + +}) diff --git a/public/javascripts/app/forms/picture_form.js b/public/javascripts/app/forms/picture_form.js new file mode 100644 index 0000000000000000000000000000000000000000..f8aaf64fd6725eea5dc9171d63ddb5a0a35abf6d --- /dev/null +++ b/public/javascripts/app/forms/picture_form.js @@ -0,0 +1,40 @@ +app.forms.Picture = app.forms.Base.extend({ + templateName : "picture-form", + + events : { + 'ajax:complete .new_photo' : "photoUploaded", + "change input[name='photo[user_file]']" : "submitForm" + }, + + initialize : function() { + this.photos = new Backbone.Collection() + this.photos.bind("add", this.render, this) + }, + + postRenderTemplate : function(){ + this.$("input[name=authenticity_token]").val($("meta[name=csrf-token]").attr("content")) + this.$("input[name=photo_ids]").val(this.photos.pluck("id")) + this.renderPhotos(); + }, + + submitForm : function (){ + this.$("form").submit(); + }, + + photoUploaded : function(evt, xhr) { + resp = JSON.parse(xhr.responseText) + if(resp.success) { + this.photos.add(new Backbone.Model(resp.data)) + } else { + alert("Upload failed! Please try again. " + resp.error); + } + }, + + renderPhotos : function(){ + var photoContainer = this.$(".photos") + this.photos.each(function(photo){ + var photoView = new app.views.Photo({model : photo}).render().el + photoContainer.append(photoView) + }) + } +}); \ No newline at end of file diff --git a/public/javascripts/app/forms/post_form.js b/public/javascripts/app/forms/post_form.js new file mode 100644 index 0000000000000000000000000000000000000000..9acc56ab4d241015a954a31cb37cb6ea07aca3a0 --- /dev/null +++ b/public/javascripts/app/forms/post_form.js @@ -0,0 +1,63 @@ +app.forms.Post = app.forms.Base.extend({ + templateName : "post-form", + formSelector : ".new-post", + + subviews : { + ".aspect_selector" : "aspectsDropdown", + ".service_selector" : "servicesSelector", + ".new_picture" : "pictureForm" + }, + + formAttrs : { + "textarea#text_with_markup" : "text", + "input.aspect_ids" : "aspect_ids", + "input.service:checked" : "services" + }, + + initialize : function() { + this.aspectsDropdown = new app.views.AspectsDropdown(); + this.servicesSelector = new app.views.ServicesSelector(); + this.pictureForm = new app.forms.Picture(); + + this.setupFormEvents(); + }, + + setModelAttributes : function(evt){ + if(evt){ evt.preventDefault(); } + var form = this.$(this.formSelector); + + this.model.set(_.inject(this.formAttrs, setValueFromField, {})) + //pass collections across + this.model.photos = this.pictureForm.photos + this.model.set({"photos": this.model.photos.toJSON() }) + + function setValueFromField(memo, attribute, selector){ + var selectors = form.find(selector); + if(selectors.length > 1) { + memo[attribute] = _.map(selectors, function(selector){ + return $(selector).val() + }) + } else { + memo[attribute] = selectors.val(); + } + return memo + } + }, + + postRenderTemplate : function() { + this.prepAndBindMentions() + }, + + prepAndBindMentions : function(){ + Mentions.initialize(this.$("textarea.text")); + Mentions.fetchContacts(); + + this.$("textarea.text").bind("textchange", $.proxy(this.updateTextWithMarkup, this)) + }, + + updateTextWithMarkup : function() { + this.$("form textarea.text").mentionsInput('val', function(markup){ + $('#text_with_markup').val(markup); + }); + } +}); \ No newline at end of file diff --git a/public/javascripts/app/models/post.js b/public/javascripts/app/models/post.js index a5beb5ecd358d55eee4e3b82173ae9120146aacf..efe9c9a8c6e74c7803914b0a7b583e169c46ef69 100644 --- a/public/javascripts/app/models/post.js +++ b/public/javascripts/app/models/post.js @@ -90,4 +90,19 @@ app.models.Post = Backbone.Model.extend({ self.trigger('interacted', this) }}); } +}, { + + frameMoods : [ + "Day" + ], + + legacyTemplateNames : [ + "status-with-photo-backdrop", + "note", + "rich-media", + "multi-photo", + "photo-backdrop", + "activity-streams-photo", + "status" + ] }); diff --git a/public/javascripts/app/models/status_message.js b/public/javascripts/app/models/status_message.js index 7a2c8120aff6913dc05643fa55f65dbd45f23555..d1af3f03eb034d0b077672cf200865943a284d7f 100644 --- a/public/javascripts/app/models/status_message.js +++ b/public/javascripts/app/models/status_message.js @@ -1 +1,24 @@ -app.models.StatusMessage = app.models.Post.extend({ }); +app.models.StatusMessage = app.models.Post.extend({ + url : function(){ + return this.isNew() ? '/status_messages' : '/posts/' + this.get("id"); + }, + + defaults : { + 'post_type' : 'StatusMessage', + 'author' : app.currentUser ? app.currentUser.attributes : {} + }, + + toJSON : function(){ + return { + status_message : _.clone(this.attributes), + aspect_ids : this.get("aspect_ids") && this.get("aspect_ids").split(","), + photos : this.photos && this.photos.pluck("id"), + services : mungeServices(this.get("services")) + } + + function mungeServices (values) { + if(!values) { return; } + return values.length > 1 ? values : [values] + } + } +}); diff --git a/public/javascripts/app/pages/framer.js b/public/javascripts/app/pages/framer.js new file mode 100644 index 0000000000000000000000000000000000000000..e75f3b1ac1c22e9026b952cef51be530031f0806 --- /dev/null +++ b/public/javascripts/app/pages/framer.js @@ -0,0 +1,30 @@ +app.pages.Framer = app.views.Base.extend({ + templateName : "framer", + + id : "post-content", + + events : { + "click button.done" : "saveFrame" + }, + + subviews : { + ".post-view" : "postView", + ".template-picker" : "templatePicker" + }, + + initialize : function(){ + this.model = app.frame + this.model.authorIsCurrentUser = function(){ return true } + + this.model.bind("change", this.render, this) + this.templatePicker = new app.views.TemplatePicker({ model: this.model }) + }, + + postView : function(){ + return app.views.Post.showFactory(this.model) + }, + + saveFrame : function(){ + this.model.save() + } +}) diff --git a/public/javascripts/app/pages/post-new.js b/public/javascripts/app/pages/post-new.js new file mode 100644 index 0000000000000000000000000000000000000000..ab8ed72a12eeed5cbeedb0c07575b348f3954fba --- /dev/null +++ b/public/javascripts/app/pages/post-new.js @@ -0,0 +1,20 @@ +app.pages.PostNew = app.views.Base.extend({ + templateName : "post-new", + + subviews : { "#new-post" : "postForm"}, + + events : { + "click button.next" : "navigateNext" + }, + + initialize : function(){ + this.model = new app.models.StatusMessage() + this.postForm = new app.forms.Post({model : this.model}) + }, + + navigateNext : function(){ + this.postForm.setModelAttributes() + app.frame = this.model; + app.router.navigate("framer", true) + } +}); diff --git a/public/javascripts/app/pages/post-viewer.js b/public/javascripts/app/pages/post-viewer.js index f43bf957fc6df10e7a1a83327518d19d11f94486..803dbe384f9ece5bb3e02a004aed18e7372026cf 100644 --- a/public/javascripts/app/pages/post-viewer.js +++ b/public/javascripts/app/pages/post-viewer.js @@ -25,12 +25,7 @@ app.pages.PostViewer = app.views.Base.extend({ this.authorView = new app.views.PostViewerAuthor({ model : this.model }); this.interactionsView = new app.views.PostViewerInteractions({ model : this.model }); this.navView = new app.views.PostViewerNav({ model : this.model }); - this.postView = new app.views.Post({ - model : this.model, - className : this.model.get("templateName") + " post loaded", - templateName : "post-viewer/content/" + this.model.get("templateName"), - attributes : {"data-template" : this.model.get("templateName")} - }); + this.postView = app.views.Post.showFactory(this.model) this.render(); }, diff --git a/public/javascripts/app/router.js b/public/javascripts/app/router.js index 30d12cc7db8d181a5c6c2eb61b6a14674f9d19df..f45d158e41ce73c84f11ba1538841e0dd66f7d47 100644 --- a/public/javascripts/app/router.js +++ b/public/javascripts/app/router.js @@ -16,26 +16,37 @@ app.Router = Backbone.Router.extend({ "followed_tags": "stream", "tags/:name": "stream", + "posts/new" : "newPost", "posts/:id": "singlePost", - "p/:id": "singlePost" + "p/:id": "singlePost", + "framer": "framer" }, stream : function() { app.stream = new app.models.Stream(); - app.page = new app.views.Stream({model : app.stream}).render(); + app.page = new app.views.Stream({model : app.stream}); app.publisher = app.publisher || new app.views.Publisher({collection : app.stream.posts}); - var streamFacesView = new app.views.StreamFaces({collection : app.stream.posts}).render(); + var streamFacesView = new app.views.StreamFaces({collection : app.stream.posts}); - $("#main_stream").html(app.page.el); - $('#selected_aspect_contacts .content').html(streamFacesView.el); + $("#main_stream").html(app.page.render().el); + $('#selected_aspect_contacts .content').html(streamFacesView.render().el); }, photos : function() { app.photos = new app.models.Photos(); - app.page = new app.views.Photos({model : app.photos}).render(); + app.page = new app.views.Photos({model : app.photos}); + $("#main_stream").html(app.page.render().el); + }, + + newPost : function(){ + var page = new app.pages.PostNew(); + $("#container").html(page.render().el) + }, - $("#main_stream").html(app.page.el); + framer : function(){ + var page = new app.pages.Framer(); + $("#container").html(page.render().el) }, singlePost : function(id) { diff --git a/public/javascripts/app/templates/aspects-dropdown.handlebars b/public/javascripts/app/templates/aspects-dropdown.handlebars new file mode 100644 index 0000000000000000000000000000000000000000..aca5b30d726fa5079c1e83919f8eb432e6c936eb --- /dev/null +++ b/public/javascripts/app/templates/aspects-dropdown.handlebars @@ -0,0 +1,18 @@ +<div class="btn-group aspects_dropdown check-group"> + <a class="btn btn-info dropdown-toggle" data-toggle="dropdown" href="#"> + <span class="text"></span> <span class="caret"></span> + </a> + <ul class="dropdown-menu"> + <li><i class='icon-ok'/><a href="#" class="public" data-aspect-id="public" data-visibility="public">Public</a></li> + <li><i class='icon-ok'/><a href="#" class="all-aspects" data-aspect-id="all_aspects" data-visibility="all-aspects">All Aspects</a></li> + + <li class="divider"></li> + {{#each current_user.aspects}} + <li><i class='icon-ok'/><a href="#" data-aspect-id="{{id}}" data-visibility="custom">{{name}}</a></li> + {{/each}} + + </ul> +</div> + +<input type="hidden" class="aspect_ids"/> + diff --git a/public/javascripts/app/templates/day.handlebars b/public/javascripts/app/templates/day.handlebars new file mode 100644 index 0000000000000000000000000000000000000000..7a9acc3fc7401fd7d4ab29b52bd3f2582439083d --- /dev/null +++ b/public/javascripts/app/templates/day.handlebars @@ -0,0 +1,2 @@ +<section class="text">{{{text}}}</section> +<section class="photo_viewer"></section> diff --git a/public/javascripts/app/templates/framer.handlebars b/public/javascripts/app/templates/framer.handlebars new file mode 100644 index 0000000000000000000000000000000000000000..ca3e884d346f737f1f9312851db95b6b0df5fa65 --- /dev/null +++ b/public/javascripts/app/templates/framer.handlebars @@ -0,0 +1,5 @@ +<div class="controls"> + <div class='template-picker'></div> + <button class="done btn-primary">done</button> +</div> +<div class="post-view"></div> \ No newline at end of file diff --git a/public/javascripts/app/templates/photo-viewer.handlebars b/public/javascripts/app/templates/photo-viewer.handlebars new file mode 100644 index 0000000000000000000000000000000000000000..1e83ff5d328ea48a9b874bc060e7f9d90f3c734e --- /dev/null +++ b/public/javascripts/app/templates/photo-viewer.handlebars @@ -0,0 +1,3 @@ +{{#each photos}} + <img src="{{sizes.large}}"/> +{{/each}} \ No newline at end of file diff --git a/public/javascripts/app/templates/picture-form.handlebars b/public/javascripts/app/templates/picture-form.handlebars new file mode 100644 index 0000000000000000000000000000000000000000..665aed54fc47e2d03f5757e56bbd13a0fb7737fc --- /dev/null +++ b/public/javascripts/app/templates/picture-form.handlebars @@ -0,0 +1,8 @@ +<form accept-charset="UTF-8" action="/photos" class="new_photo" data-remote="true" enctype="multipart/form-data" method="post"> + <input name="authenticity_token" type="hidden"/> + <div style="margin:0;padding:0;display:inline"> + <input name="utf8" type="hidden" value="✓"/> + </div> + <input name="photo[user_file]" type="file"/> + <div class="photos"></div> +</form> diff --git a/public/javascripts/app/templates/post-form.handlebars b/public/javascripts/app/templates/post-form.handlebars new file mode 100644 index 0000000000000000000000000000000000000000..e9f0ee4b608d895d57976bdf86b3ab50c1a31767 --- /dev/null +++ b/public/javascripts/app/templates/post-form.handlebars @@ -0,0 +1,24 @@ +<div class='row'> + <div class='span8 offset2 new-post-section'> + + <div class="new_picture"/> + + <form class="new-post"> + <fieldset> + <legend> + New Post + </legend> + <label> + text + <textarea class="text span8"/> + </label> + <textarea id="text_with_markup" style="display:none;"/> + + <div class="aspect_selector"></div> + <div class="service_selector"></div> + + <input type="submit" class="btn-primary" value="Share" /> + </fieldset> + </form> + </div> +</div> diff --git a/public/javascripts/app/templates/post-new.handlebars b/public/javascripts/app/templates/post-new.handlebars new file mode 100644 index 0000000000000000000000000000000000000000..ab2b6264ae430eff37f7ce88db9af14ded5adfda --- /dev/null +++ b/public/javascripts/app/templates/post-new.handlebars @@ -0,0 +1,4 @@ +<div class="container"> + <div id="new-post"></div> + <button class="btn-primary next">Next</button> +</div> diff --git a/public/javascripts/app/templates/services-selector.handlebars b/public/javascripts/app/templates/services-selector.handlebars new file mode 100644 index 0000000000000000000000000000000000000000..a8fd3c827c6c73186f04e885a83c7a858978d124 --- /dev/null +++ b/public/javascripts/app/templates/services-selector.handlebars @@ -0,0 +1,7 @@ +Broadcast: +{{#each current_user.services}} + <input type="checkbox" name="services" class="service" value="{{provider}}" /> + <img src="/images/social_media_logos/{{provider}}-16x16.png" /> + <br /> +{{/each}} +<br /> diff --git a/public/javascripts/app/templates/stream-element.handlebars b/public/javascripts/app/templates/stream-element.handlebars index 2e0fe9a838745db28abceddf2bbcf39860afaa9c..024c51a0b6fe8fb1b74d1047a0df04dafb636057 100644 --- a/public/javascripts/app/templates/stream-element.handlebars +++ b/public/javascripts/app/templates/stream-element.handlebars @@ -2,7 +2,7 @@ {{#if current_user}} <div class="controls"> - {{#if authorIsNotCurrentUser}} + {{#unless authorIsCurrentUser}} <a href="#" rel=nofollow> <img src="{{imageUrl "/images/icons/ignoreuser.png"}}"" alt="Ignoreuser" class="block_user control_icon" title="{{t "ignore"}}" /> </a> @@ -13,7 +13,7 @@ <a href="#" rel=nofollow> <img src="{{imageUrl "/images/deletelabel.png"}}" class="delete control_icon remove_post" title="{{t "delete"}}" /> </a> - {{/if}} + {{/unless}} </div> {{/if}} diff --git a/public/javascripts/app/templates/template-picker.handlebars b/public/javascripts/app/templates/template-picker.handlebars new file mode 100644 index 0000000000000000000000000000000000000000..9617e86f212f914f0c3c87c7b6d86988f5a6ff44 --- /dev/null +++ b/public/javascripts/app/templates/template-picker.handlebars @@ -0,0 +1,5 @@ +<select name="template"> + {{#each templates}} + <option value="{{.}}">{{.}}</option> + {{/each}} +</select> \ No newline at end of file diff --git a/public/javascripts/app/views.js b/public/javascripts/app/views.js index 6ebd412e0b3c71476ff9038f1d2306702819a04b..faadd32ce5ac946185f14b94e0f129e1943c58dd 100644 --- a/public/javascripts/app/views.js +++ b/public/javascripts/app/views.js @@ -9,7 +9,10 @@ app.views.Base = Backbone.View.extend({ }, setupRenderEvents : function(){ - this.model.bind('remove', this.remove, this); + if(this.model) { + //this should be in streamobjects view + this.model.bind('remove', this.remove, this); + } // this line is too generic. we usually only want to re-render on // feedback changes as the post content, author, and time do not change. @@ -18,7 +21,7 @@ app.views.Base = Backbone.View.extend({ }, defaultPresenter : function(){ - var modelJson = this.model ? this.model.toJSON() : {} + var modelJson = this.model ? _.clone(this.model.attributes) : {} return _.extend(modelJson, { current_user : app.currentUser.attributes, loggedIn : app.currentUser.authenticated() @@ -37,7 +40,9 @@ app.views.Base = Backbone.View.extend({ renderTemplate : function(){ var presenter = _.isFunction(this.presenter) ? this.presenter() : this.presenter this.template = JST[this.templateName] - $(this.el).html(this.template(presenter)); + $(this.el) + .html(this.template(presenter)) + .attr("data-template", _.last(this.templateName.split("/"))); this.postRenderTemplate(); }, diff --git a/public/javascripts/app/views/aspects_dropdown_view.js b/public/javascripts/app/views/aspects_dropdown_view.js new file mode 100644 index 0000000000000000000000000000000000000000..a6085263f2bf68714eee56e92612143f06bf0d39 --- /dev/null +++ b/public/javascripts/app/views/aspects_dropdown_view.js @@ -0,0 +1,68 @@ +app.views.AspectsDropdown = app.views.Base.extend({ + templateName : "aspects-dropdown", + events : { + "click .dropdown-menu a" : "setVisibility" + }, + + postRenderTemplate : function(){ + this.setVisibility({target : this.$("a[data-visibility='all-aspects']").first()}) + }, + + setVisibility : function(evt){ + var self = this + , link = $(evt.target).closest("a") + , visibilityCallbacks = { + 'public' : setPublic, + 'all-aspects' : setPrivate, + 'custom' : setCustom + } + + visibilityCallbacks[link.data("visibility")]() + + this.setAspectIds() + + function setPublic (){ + deselectAll() + selectAspect() + self.setDropdownText(link.text()) + } + + function setPrivate (){ + deselectAll() + selectAspect() + self.setDropdownText(link.text()) + } + + function setCustom (){ + deselectOverrides() + link.parents("li").toggleClass("selected") + self.setDropdownText(link.text()) + evt.stopImmediatePropagation(); + } + + function selectAspect() { + link.parents("li").addClass("selected") + } + + function deselectOverrides() { + self.$("a.public, a.all-aspects").parent().removeClass("selected") + } + + function deselectAll() { + self.$("li.selected").removeClass("selected") + } + }, + + setDropdownText : function(text){ + $.trim(this.$(".dropdown-toggle .text").text(text)) + }, + + setAspectIds : function(){ + var selectedAspects = this.$("li.selected a") + var aspectIds = _.map(selectedAspects, function(aspect){ + return $(aspect).data("aspect-id")} + ) + + this.$("input.aspect_ids").val(aspectIds) + } +}) diff --git a/public/javascripts/app/views/photo_viewer.js b/public/javascripts/app/views/photo_viewer.js new file mode 100644 index 0000000000000000000000000000000000000000..3f30cfbd469dbaaaa6c78178dbfce8827b9f88a3 --- /dev/null +++ b/public/javascripts/app/views/photo_viewer.js @@ -0,0 +1,7 @@ +app.views.PhotoViewer = app.views.Base.extend({ + templateName : "photo-viewer", + + presenter : function(){ + return { photos : this.model.get("photos") } //json array of attributes, not backbone models, yet. + } +}); \ No newline at end of file diff --git a/public/javascripts/app/views/post/day_view.js b/public/javascripts/app/views/post/day_view.js new file mode 100644 index 0000000000000000000000000000000000000000..f01bb41a9f81f6a6c9ef28fffa280fdc856c4afa --- /dev/null +++ b/public/javascripts/app/views/post/day_view.js @@ -0,0 +1,16 @@ +app.views.Post.Day = app.views.Post.extend({ + templateName : "day", + className : "day post loaded", + + subviews : { "section.photo_viewer" : "photoViewer" }, + + photoViewer : function(){ + return new app.views.PhotoViewer({ model : this.model }) + }, + + postRenderTemplate : function(){ + if(this.model.get("text").length < 140){ + this.$('section.text').addClass('headline'); + } + } +}); \ No newline at end of file diff --git a/public/javascripts/app/views/post_view.js b/public/javascripts/app/views/post_view.js index 5900bf75c39c2669e258d2d2d371ef9f6c65da41..4f167fd3ae030b400d28748f08716d3e61756259 100644 --- a/public/javascripts/app/views/post_view.js +++ b/public/javascripts/app/views/post_view.js @@ -1,133 +1,44 @@ app.views.Post = app.views.StreamObject.extend({ - - templateName: "stream-element", - - className : "stream_element loaded", - - events: { - "click .focus_comment_textarea": "focusCommentTextarea", - "click .show_nsfw_post": "removeNsfwShield", - "click .toggle_nsfw_state": "toggleNsfwState", - - "click .remove_post": "destroyModel", - "click .hide_post": "hidePost", - "click .block_user": "blockUser" - }, - - subviews : { - ".feedback" : "feedbackView", - ".likes" : "likesInfoView", - ".comments" : "commentStreamView", - ".post-content" : "postContentView" - }, - - tooltipSelector : ".delete, .block_user, .post_scope", - - initialize : function(options) { - // allow for a custom template name to be passed in via the options hash - this.templateName = options.templateName || this.templateName - - this.model.bind('remove', this.remove, this); - this.model.bind('destroy', this.destroy, this); - - //subviews - this.commentStreamView = new app.views.CommentStream({ model : this.model}); - - return this; - }, - - likesInfoView : function(){ - return new app.views.LikesInfo({ model : this.model}); - }, - - feedbackView : function(){ - if(!app.currentUser.authenticated()) { return null } - return new app.views.Feedback({model : this.model}); - }, - - postContentView: function(){ - var normalizedClass = this.model.get("post_type").replace(/::/, "__"); - var postClass = app.views[normalizedClass] || app.views.StatusMessage; - return new postClass({ model : this.model }); - }, - presenter : function() { return _.extend(this.defaultPresenter(), { - authorIsNotCurrentUser : this.authorIsNotCurrentUser(), + authorIsCurrentUser : this.authorIsCurrentUser(), showPost : this.showPost(), text : app.helpers.textFormatter(this.model) }) }, - showPost : function() { - return (app.currentUser.get("showNsfw")) || !this.model.get("nsfw") - }, - - removeNsfwShield: function(evt){ - if(evt){ evt.preventDefault(); } - this.model.set({nsfw : false}) - this.render(); + authorIsCurrentUser : function() { + return app.currentUser.authenticated() && this.model.get("author").id == app.user().id }, - toggleNsfwState: function(evt){ - if(evt){ evt.preventDefault(); } - app.currentUser.toggleNsfwState(); - }, - - blockUser: function(evt){ - if(evt) { evt.preventDefault(); } - if(!confirm("Ignore this user?")) { return } - - var personId = this.model.get("author").id; - var block = new app.models.Block(); - - block.save({block : {person_id : personId}}, { - success : function(){ - if(!app.stream) { return } - - _.each(app.stream.posts.models, function(model){ - if(model.get("author").id == personId) { - app.stream.posts.remove(model); - } - }) - } - }) - }, - - hidePost : function(evt) { - if(evt) { evt.preventDefault(); } - if(!confirm(Diaspora.I18n.t('confirm_dialog'))) { return } - - $.ajax({ - url : "/share_visibilities/42", - type : "PUT", - data : { - post_id : this.model.id - } - }) - - this.slideAndRemove(); - }, - - focusCommentTextarea: function(evt){ - evt.preventDefault(); - this.$(".new_comment_form_wrapper").removeClass("hidden"); - this.$(".comment_box").focus(); - - return this; - }, + showPost : function() { + return (app.currentUser.get("showNsfw")) || !this.model.get("nsfw") + } +}, { //static methods below - authorIsNotCurrentUser : function() { - return this.model.get("author").id != app.user().id - }, + showFactory : function(model) { + var frameName = model.get("frame_name"); - isOnShowPage : function() { - return (!this.model.collection) && (this.model.url() == document.location.pathname); - }, + if(_.include(app.models.Post.legacyTemplateNames, frameName)){ + return legacyShow(model) + } else { + return new app.views.Post[frameName]({ + model : model + }) + } - destroy : function() { - if (this.isOnShowPage()) { - document.location.replace(Backbone.history.options.root); + function legacyShow(model) { + return new app.views.Post.Legacy({ + model : model, + className : frameName + " post loaded", + templateName : "post-viewer/content/" + frameName + }); } } }); + +app.views.Post.Legacy = app.views.Post.extend({ + initialize : function(options) { + this.templateName = options.templateName || this.templateName + } +}) \ No newline at end of file diff --git a/public/javascripts/app/views/publisher_view.js b/public/javascripts/app/views/publisher_view.js index 963949338eb50f000c7abf8a01d07cbb873d1a89..2274aeb746ed588ea5b6facff2c192210b2970bf 100644 --- a/public/javascripts/app/views/publisher_view.js +++ b/public/javascripts/app/views/publisher_view.js @@ -22,8 +22,9 @@ app.views.Publisher = Backbone.View.extend({ var serializedForm = $(evt.target).closest("form").serializeObject(); - // save status message - var statusMessage = new app.models.StatusMessage(); + // lulz this code should be killed. + var statusMessage = new app.models.Post(); + statusMessage.save({ "status_message" : { "text" : serializedForm["status_message[text]"] diff --git a/public/javascripts/app/views/services_selector_view.js b/public/javascripts/app/views/services_selector_view.js new file mode 100644 index 0000000000000000000000000000000000000000..7ed3e3c99cad834ab34061c6f952b6ff2c771b85 --- /dev/null +++ b/public/javascripts/app/views/services_selector_view.js @@ -0,0 +1,5 @@ +app.views.ServicesSelector = app.views.Base.extend({ + + templateName : "services-selector" + +}); \ No newline at end of file diff --git a/public/javascripts/app/views/stream_object_view.js b/public/javascripts/app/views/stream_object_view.js index 1f46e732b50641b891d7fb3e5a4e2f0154085ee2..42d7bc6d4195dd420307386c9f59e350aae041c4 100644 --- a/public/javascripts/app/views/stream_object_view.js +++ b/public/javascripts/app/views/stream_object_view.js @@ -1,6 +1,5 @@ app.views.StreamObject = app.views.Base.extend({ - - destroyModel: function(evt) { + destroyModel: function(evt) { if (evt) { evt.preventDefault(); } diff --git a/public/javascripts/app/views/stream_post_views.js b/public/javascripts/app/views/stream_post_views.js new file mode 100644 index 0000000000000000000000000000000000000000..5d955c54de447c7b39dc4db5b1bf3c96188a0cf6 --- /dev/null +++ b/public/javascripts/app/views/stream_post_views.js @@ -0,0 +1,102 @@ +app.views.StreamPost = app.views.Post.extend({ + templateName: "stream-element", + className : "stream_element loaded", + + subviews : { + ".feedback" : "feedbackView", + ".likes" : "likesInfoView", + ".comments" : "commentStreamView", + ".post-content" : "postContentView" + }, + + events: { + "click .focus_comment_textarea": "focusCommentTextarea", + "click .show_nsfw_post": "removeNsfwShield", + "click .toggle_nsfw_state": "toggleNsfwState", + + "click .remove_post": "destroyModel", + "click .hide_post": "hidePost", + "click .block_user": "blockUser" + }, + + tooltipSelector : ".delete, .block_user, .post_scope", + + initialize : function(){ + this.model.bind('remove', this.remove, this); + + //subviews + this.commentStreamView = new app.views.CommentStream({ model : this.model}); + }, + + + likesInfoView : function(){ + return new app.views.LikesInfo({ model : this.model}); + }, + + feedbackView : function(){ + if(!app.currentUser.authenticated()) { return null } + return new app.views.Feedback({model : this.model}); + }, + + postContentView: function(){ + var normalizedClass = this.model.get("post_type").replace(/::/, "__"); + var postClass = app.views[normalizedClass] || app.views.StatusMessage; + return new postClass({ model : this.model }); + }, + + removeNsfwShield: function(evt){ + if(evt){ evt.preventDefault(); } + this.model.set({nsfw : false}) + this.render(); + }, + + toggleNsfwState: function(evt){ + if(evt){ evt.preventDefault(); } + app.currentUser.toggleNsfwState(); + }, + + + blockUser: function(evt){ + if(evt) { evt.preventDefault(); } + if(!confirm("Ignore this user?")) { return } + + var personId = this.model.get("author").id; + var block = new app.models.Block(); + + block.save({block : {person_id : personId}}, { + success : function(){ + if(!app.stream) { return } + + _.each(app.stream.posts.models, function(model){ + if(model.get("author").id == personId) { + app.stream.posts.remove(model); + } + }) + } + }) + }, + + hidePost : function(evt) { + if(evt) { evt.preventDefault(); } + if(!confirm(Diaspora.I18n.t('confirm_dialog'))) { return } + + $.ajax({ + url : "/share_visibilities/42", + type : "PUT", + data : { + post_id : this.model.id + } + }) + + this.slideAndRemove(); + }, + + focusCommentTextarea: function(evt){ + evt.preventDefault(); + this.$(".new_comment_form_wrapper").removeClass("hidden"); + this.$(".comment_box").focus(); + + return this; + } + +}) \ No newline at end of file diff --git a/public/javascripts/app/views/stream_view.js b/public/javascripts/app/views/stream_view.js index a39892dd96961407fc543edf39b5c25e64f76ea5..b17ae4c60a6efa083d849a2f328c01593b50b828 100644 --- a/public/javascripts/app/views/stream_view.js +++ b/public/javascripts/app/views/stream_view.js @@ -27,7 +27,7 @@ app.views.Stream = Backbone.View.extend({ }, addPost : function(post) { - var postView = new app.views.Post({ model: post }); + var postView = new app.views.StreamPost({ model: post }); $(this.el)[ (this.collection.at(0).id == post.id) diff --git a/public/javascripts/app/views/template_picker_view.js b/public/javascripts/app/views/template_picker_view.js new file mode 100644 index 0000000000000000000000000000000000000000..834bd72e776b73c72c7d5c3a3160dbd0db66f907 --- /dev/null +++ b/public/javascripts/app/views/template_picker_view.js @@ -0,0 +1,25 @@ +app.views.TemplatePicker = app.views.Base.extend({ + templateName : "template-picker", + + initialize : function(){ + this.model.set({frame_name : 'status'}) + }, + + events : { + "change select" : "setModelTemplate" + }, + + postRenderTemplate : function(){ + this.$("select[name=template]").val(this.model.get("frame_name")) + }, + + setModelTemplate : function(evt){ + this.model.set({"frame_name": this.$("select[name=template]").val()}) + }, + + presenter : function() { + return _.extend(this.defaultPresenter(), { + templates : _.union(app.models.Post.frameMoods, app.models.Post.legacyTemplateNames) + }) + } +}) \ No newline at end of file diff --git a/public/javascripts/mentions.js b/public/javascripts/mentions.js index 449a57f079df035a290dd64feb923d0e8a6c734c..b6e23043734f76ee04ab7f19a50ac7f35c6ef578 100644 --- a/public/javascripts/mentions.js +++ b/public/javascripts/mentions.js @@ -1,10 +1,10 @@ var Mentions = { initialize: function(mentionsInput) { - mentionsInput.mentionsInput(Mentions.options); + return mentionsInput.mentionsInput(Mentions.options); }, fetchContacts : function(){ - Mentions.contacts || $.getJSON($(".selected_contacts_link").attr("href"), function(data) { + Mentions.contacts || $.getJSON("/contacts", function(data) { Mentions.contacts = data; }); }, diff --git a/public/javascripts/rails.js b/public/javascripts/rails.js index 2fbb9b83c06cc9339d4a82d28c02cb23a1fd41b3..06b4e0b53a07a1b3654ba00d39c271b0c57f412b 100644 --- a/public/javascripts/rails.js +++ b/public/javascripts/rails.js @@ -1,198 +1,373 @@ -/* Clear form plugin - called using $("elem").clearForm(); */ -$.fn.clearForm = function() { - return this.each(function() { - if ($(this).is('form')) { - return $(':input', this).clearForm(); - } - if ($(this).hasClass('clear_on_submit') || $(this).is(':text') || $(this).is(':password') || $(this).is('textarea')) { - $(this).val(''); - } else if ($(this).is(':checkbox') || $(this).is(':radio')) { - $(this).attr('checked', false); - } else if ($(this).is('select')) { - this.selectedIndex = -1; - } else if ($(this).attr('name') == 'photos[]') { - $(this).val(''); - } - $(this).blur(); - }); -}; +(function($, undefined) { /** * Unobtrusive scripting adapter for jQuery * - * Requires jQuery 1.4.3 or later. + * Requires jQuery 1.6.0 or later. * https://github.com/rails/jquery-ujs + + * Uploading file using rails.js + * ============================= + * + * By default, browsers do not allow files to be uploaded via AJAX. As a result, if there are any non-blank file fields + * in the remote form, this adapter aborts the AJAX submission and allows the form to submit through standard means. + * + * The `ajax:aborted:file` event allows you to bind your own handler to process the form submission however you wish. + * + * Ex: + * $('form').live('ajax:aborted:file', function(event, elements){ + * // Implement own remote file-transfer handler here for non-blank file inputs passed in `elements`. + * // Returning false in this handler tells rails.js to disallow standard form submission + * return false; + * }); + * + * The `ajax:aborted:file` event is fired when a file-type input is detected with a non-blank value. + * + * Third-party tools can use this hook to detect when an AJAX file upload is attempted, and then use + * techniques like the iframe method to upload the file instead. + * + * Required fields in rails.js + * =========================== + * + * If any blank required inputs (required="required") are detected in the remote form, the whole form submission + * is canceled. Note that this is unlike file inputs, which still allow standard (non-AJAX) form submission. + * + * The `ajax:aborted:required` event allows you to bind your own handler to inform the user of blank required inputs. + * + * !! Note that Opera does not fire the form's submit event if there are blank required inputs, so this event may never + * get fired in Opera. This event is what causes other browsers to exhibit the same submit-aborting behavior. + * + * Ex: + * $('form').live('ajax:aborted:required', function(event, elements){ + * // Returning false in this handler tells rails.js to submit the form anyway. + * // The blank required inputs are passed to this function in `elements`. + * return ! confirm("Would you like to submit the form with missing info?"); + * }); */ -(function($) { - // Make sure that every Ajax request sends the CSRF token - function CSRFProtection(fn) { - var token = $('meta[name="csrf-token"]').attr('content'); - if (token) fn(function(xhr) { xhr.setRequestHeader('X-CSRF-Token', token) }); - } - if ($().jquery == '1.5') { // gruesome hack - var factory = $.ajaxSettings.xhr; - $.ajaxSettings.xhr = function() { - var xhr = factory(); - CSRFProtection(function(setHeader) { - var open = xhr.open; - xhr.open = function() { open.apply(this, arguments); setHeader(this) }; + // Shorthand to make it a little easier to call public rails functions from within rails.js + var rails; + + $.rails = rails = { + // Link elements bound by jquery-ujs + linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with]', + + // Select elements bound by jquery-ujs + inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]', + + // Form elements bound by jquery-ujs + formSubmitSelector: 'form', + + // Form input elements bound by jquery-ujs + formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not(button[type])', + + // Form input elements disabled during form submission + disableSelector: 'input[data-disable-with], button[data-disable-with], textarea[data-disable-with]', + + // Form input elements re-enabled after form submission + enableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled', + + // Form required input elements + requiredInputSelector: 'input[name][required]:not([disabled]),textarea[name][required]:not([disabled])', + + // Form file input elements + fileInputSelector: 'input:file', + + // Link onClick disable selector with possible reenable after remote submission + linkDisableSelector: 'a[data-disable-with]', + + // Make sure that every Ajax request sends the CSRF token + CSRFProtection: function(xhr) { + var token = $('meta[name="csrf-token"]').attr('content'); + if (token) xhr.setRequestHeader('X-CSRF-Token', token); + }, + + // Triggers an event on an element and returns false if the event result is false + fire: function(obj, name, data) { + var event = $.Event(name); + obj.trigger(event, data); + return event.result !== false; + }, + + // Default confirm dialog, may be overridden with custom confirm dialog in $.rails.confirm + confirm: function(message) { + return confirm(message); + }, + + // Default ajax function, may be overridden with custom function in $.rails.ajax + ajax: function(options) { + return $.ajax(options); + }, + + // Submits "remote" forms and links with ajax + handleRemote: function(element) { + var method, url, data, + crossDomain = element.data('cross-domain') || null, + dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType), + options; + + if (rails.fire(element, 'ajax:before')) { + + if (element.is('form')) { + method = element.attr('method'); + url = element.attr('action'); + data = element.serializeArray(); + // memoized value from clicked submit button + var button = element.data('ujs:submit-button'); + if (button) { + data.push(button); + element.data('ujs:submit-button', null); + } + } else if (element.is(rails.inputChangeSelector)) { + method = element.data('method'); + url = element.data('url'); + data = element.serialize(); + if (element.data('params')) data = data + "&" + element.data('params'); + } else { + method = element.data('method'); + url = element.attr('href'); + data = element.data('params') || null; + } + + options = { + type: method || 'GET', data: data, dataType: dataType, crossDomain: crossDomain, + // stopping the "ajax:beforeSend" event will cancel the ajax request + beforeSend: function(xhr, settings) { + if (settings.dataType === undefined) { + xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script); + } + return rails.fire(element, 'ajax:beforeSend', [xhr, settings]); + }, + success: function(data, status, xhr) { + element.trigger('ajax:success', [data, status, xhr]); + }, + complete: function(xhr, status) { + element.trigger('ajax:complete', [xhr, status]); + }, + error: function(xhr, status, error) { + element.trigger('ajax:error', [xhr, status, error]); + } + }; + // Only pass url to `ajax` options if not blank + if (url) { options.url = url; } + + return rails.ajax(options); + } else { + return false; + } + }, + + // Handles "data-method" on links such as: + // <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a> + handleMethod: function(link) { + var href = link.attr('href'), + method = link.data('method'), + target = link.attr('target'), + csrf_token = $('meta[name=csrf-token]').attr('content'), + csrf_param = $('meta[name=csrf-param]').attr('content'), + form = $('<form method="post" action="' + href + '"></form>'), + metadata_input = '<input name="_method" value="' + method + '" type="hidden" />'; + + if (csrf_param !== undefined && csrf_token !== undefined) { + metadata_input += '<input name="' + csrf_param + '" value="' + csrf_token + '" type="hidden" />'; + } + + if (target) { form.attr('target', target); } + + form.hide().append(metadata_input).appendTo('body'); + form.submit(); + }, + + /* Disables form elements: + - Caches element value in 'ujs:enable-with' data store + - Replaces element text with value of 'data-disable-with' attribute + - Sets disabled property to true + */ + disableFormElements: function(form) { + form.find(rails.disableSelector).each(function() { + var element = $(this), method = element.is('button') ? 'html' : 'val'; + element.data('ujs:enable-with', element[method]()); + element[method](element.data('disable-with')); + element.prop('disabled', true); }); - return xhr; - }; - } - else $(document).ajaxSend(function(e, xhr) { - CSRFProtection(function(setHeader) { setHeader(xhr) }); - }); + }, + + /* Re-enables disabled form elements: + - Replaces element text with cached value from 'ujs:enable-with' data store (created in `disableFormElements`) + - Sets disabled property to false + */ + enableFormElements: function(form) { + form.find(rails.enableSelector).each(function() { + var element = $(this), method = element.is('button') ? 'html' : 'val'; + if (element.data('ujs:enable-with')) element[method](element.data('ujs:enable-with')); + element.prop('disabled', false); + }); + }, + + /* For 'data-confirm' attribute: + - Fires `confirm` event + - Shows the confirmation dialog + - Fires the `confirm:complete` event - // Triggers an event on an element and returns the event result - function fire(obj, name, data) { - var event = new $.Event(name); - obj.trigger(event, data); - return event.result !== false; - } - - // Submits "remote" forms and links with ajax - function handleRemote(element) { - var method, url, data, - dataType = element.attr('data-type') || ($.ajaxSettings && $.ajaxSettings.dataType); - - if (element.is('form')) { - method = element.attr('method'); - url = element.attr('action'); - data = element.serializeArray(); - // memoized value from clicked submit button - var button = element.data('ujs:submit-button'); - if (button) { - data.push(button); - element.data('ujs:submit-button', null); + Returns `true` if no function stops the chain and user chose yes; `false` otherwise. + Attaching a handler to the element's `confirm` event that returns a `falsy` value cancels the confirmation dialog. + Attaching a handler to the element's `confirm:complete` event that returns a `falsy` value makes this function + return false. The `confirm:complete` event is fired whether or not the user answered true or false to the dialog. + */ + allowAction: function(element) { + var message = element.data('confirm'), + answer = false, callback; + if (!message) { return true; } + + if (rails.fire(element, 'confirm')) { + answer = rails.confirm(message); + callback = rails.fire(element, 'confirm:complete', [answer]); } - } else { - method = element.attr('data-method'); - url = element.attr('href'); - data = null; - } + return answer && callback; + }, - $.ajax({ - url: url, type: method || 'GET', data: data, dataType: dataType, - // stopping the "ajax:beforeSend" event will cancel the ajax request - beforeSend: function(xhr, settings) { - if (settings.dataType === undefined) { - xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script); + // Helper function which checks for blank inputs in a form that match the specified CSS selector + blankInputs: function(form, specifiedSelector, nonBlank) { + var inputs = $(), input, + selector = specifiedSelector || 'input,textarea'; + form.find(selector).each(function() { + input = $(this); + // Collect non-blank inputs if nonBlank option is true, otherwise, collect blank inputs + if (nonBlank ? input.val() : !input.val()) { + inputs = inputs.add(input); } - return fire(element, 'ajax:beforeSend', [xhr, settings]); - }, - success: function(data, status, xhr) { - element.trigger('ajax:success', [data, status, xhr]); - }, - complete: function(xhr, status) { - element.trigger('ajax:complete', [xhr, status]); - }, - error: function(xhr, status, error) { - element.trigger('ajax:error', [xhr, status, error]); + }); + return inputs.length ? inputs : false; + }, + + // Helper function which checks for non-blank inputs in a form that match the specified CSS selector + nonBlankInputs: function(form, specifiedSelector) { + return rails.blankInputs(form, specifiedSelector, true); // true specifies nonBlank + }, + + // Helper function, needed to provide consistent behavior in IE + stopEverything: function(e) { + $(e.target).trigger('ujs:everythingStopped'); + e.stopImmediatePropagation(); + return false; + }, + + // find all the submit events directly bound to the form and + // manually invoke them. If anyone returns false then stop the loop + callFormSubmitBindings: function(form, event) { + var events = form.data('events'), continuePropagation = true; + if (events !== undefined && events['submit'] !== undefined) { + $.each(events['submit'], function(i, obj){ + if (typeof obj.handler === 'function') return continuePropagation = obj.handler(event); + }); } - }); - } - - // Handles "data-method" on links such as: - // <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a> - function handleMethod(link) { - var href = link.attr('href'), - method = link.attr('data-method'), - csrf_token = $('meta[name=csrf-token]').attr('content'), - csrf_param = $('meta[name=csrf-param]').attr('content'), - form = $('<form method="post" action="' + href + '"></form>'), - metadata_input = '<input name="_method" value="' + method + '" type="hidden" />', - form_params = link.data('form-params'); - - if (csrf_param !== undefined && csrf_token !== undefined) { - metadata_input += '<input name="' + csrf_param + '" value="' + csrf_token + '" type="hidden" />'; - } + return continuePropagation; + }, + + // replace element's html with the 'data-disable-with' after storing original html + // and prevent clicking on it + disableElement: function(element) { + element.data('ujs:enable-with', element.html()); // store enabled state + element.html(element.data('disable-with')); // set to disabled state + element.bind('click.railsDisable', function(e) { // prevent further clicking + return rails.stopEverything(e) + }); + }, - // support non-nested JSON encoded params for links - if (form_params != undefined) { - var params = $.parseJSON(form_params); - for (key in params) { - form.append($("<input>").attr({"type": "hidden", "name": key, "value": params[key]})); + // restore element to its original state which was disabled by 'disableElement' above + enableElement: function(element) { + if (element.data('ujs:enable-with') !== undefined) { + element.html(element.data('ujs:enable-with')); // set to old enabled state + // this should be element.removeData('ujs:enable-with') + // but, there is currently a bug in jquery which makes hyphenated data attributes not get removed + element.data('ujs:enable-with', false); // clean up cache } + element.unbind('click.railsDisable'); // enable element } - form.hide().append(metadata_input).appendTo('body'); - form.submit(); - } - - function disableFormElements(form) { - form.find('input[data-disable-with]').each(function() { - var input = $(this); - input.data('ujs:enable-with', input.val()) - .val(input.attr('data-disable-with')) - .attr('disabled', 'disabled'); - }); - } - - function enableFormElements(form) { - form.find('input[data-disable-with]').each(function() { - var input = $(this); - input.val(input.data('ujs:enable-with')).removeAttr('disabled'); - }); - } - - function allowAction(element) { - var message = element.attr('data-confirm'); - return !message || (fire(element, 'confirm') && confirm(message)); - } - - function requiredValuesMissing(form) { - var missing = false; - form.find('input[name][required]').each(function() { - if (!$(this).val()) missing = true; - }); - return missing; - } - - $('a[data-confirm], a[data-method], a[data-remote]').live('click.rails', function(e) { - var link = $(this); - if (!allowAction(link)) return false; + }; + + $.ajaxPrefilter(function(options, originalOptions, xhr){ if ( !options.crossDomain ) { rails.CSRFProtection(xhr); }}); + + $(document).delegate(rails.linkDisableSelector, 'ajax:complete', function() { + rails.enableElement($(this)); + }); - if (link.attr('data-remote') != undefined) { - handleRemote(link); + $(document).delegate(rails.linkClickSelector, 'click.rails', function(e) { + var link = $(this), method = link.data('method'), data = link.data('params'); + if (!rails.allowAction(link)) return rails.stopEverything(e); + + if (link.is(rails.linkDisableSelector)) rails.disableElement(link); + + if (link.data('remote') !== undefined) { + if ( (e.metaKey || e.ctrlKey) && (!method || method === 'GET') && !data ) { return true; } + + if (rails.handleRemote(link) === false) { rails.enableElement(link); } return false; - } else if (link.attr('data-method')) { - handleMethod(link); + + } else if (link.data('method')) { + rails.handleMethod(link); return false; } }); - $('form').live('submit.rails', function(e) { - var form = $(this), remote = form.attr('data-remote') != undefined; - if (!allowAction(form)) return false; + $(document).delegate(rails.inputChangeSelector, 'change.rails', function(e) { + var link = $(this); + if (!rails.allowAction(link)) return rails.stopEverything(e); + + rails.handleRemote(link); + return false; + }); + + $(document).delegate(rails.formSubmitSelector, 'submit.rails', function(e) { + var form = $(this), + remote = form.data('remote') !== undefined, + blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector), + nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector); - // skip other logic when required values are missing - if (requiredValuesMissing(form)) return !remote; + if (!rails.allowAction(form)) return rails.stopEverything(e); + + // skip other logic when required values are missing or file upload is present + if (blankRequiredInputs && form.attr("novalidate") == undefined && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) { + return rails.stopEverything(e); + } if (remote) { - handleRemote(form); + if (nonBlankFileInputs) { + return rails.fire(form, 'ajax:aborted:file', [nonBlankFileInputs]); + } + + // If browser does not support submit bubbling, then this live-binding will be called before direct + // bindings. Therefore, we should directly call any direct bindings before remotely submitting form. + if (!$.support.submitBubbles && $().jquery < '1.7' && rails.callFormSubmitBindings(form, e) === false) return rails.stopEverything(e); + + rails.handleRemote(form); return false; + } else { // slight timeout so that the submit button gets properly serialized - setTimeout(function(){ disableFormElements(form) }, 13); + setTimeout(function(){ rails.disableFormElements(form); }, 13); } }); - $('form input[type=submit], form button[type=submit], form button:not([type])').live('click.rails', function() { + $(document).delegate(rails.formInputClickSelector, 'click.rails', function(event) { var button = $(this); - if (!allowAction(button)) return false; + + if (!rails.allowAction(button)) return rails.stopEverything(event); + // register the pressed submit button - var name = button.attr('name'), data = name ? {name:name, value:button.val()} : null; + var name = button.attr('name'), + data = name ? {name:name, value:button.val()} : null; + button.closest('form').data('ujs:submit-button', data); }); - $('form').live('ajax:beforeSend.rails', function(event) { - if (this == event.target) disableFormElements($(this)); + $(document).delegate(rails.formSubmitSelector, 'ajax:beforeSend.rails', function(event) { + if (this == event.target) rails.disableFormElements($(this)); }); - $('form').live('ajax:complete.rails', function(event) { - if (this == event.target) enableFormElements($(this)); + $(document).delegate(rails.formSubmitSelector, 'ajax:complete.rails', function(event) { + if (this == event.target) rails.enableFormElements($(this)); }); -})( jQuery ); +})( jQuery ); diff --git a/public/javascripts/vendor/bootstrap/bootstrap-dropdown.js b/public/javascripts/vendor/bootstrap/bootstrap-dropdown.js new file mode 100644 index 0000000000000000000000000000000000000000..f80995ca2f9a936199a35c3cf5ef669c76f6a6fa --- /dev/null +++ b/public/javascripts/vendor/bootstrap/bootstrap-dropdown.js @@ -0,0 +1,92 @@ +/* ============================================================ + * bootstrap-dropdown.js v2.0.1 + * http://twitter.github.com/bootstrap/javascript.html#dropdowns + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function( $ ){ + + "use strict" + + /* DROPDOWN CLASS DEFINITION + * ========================= */ + + var toggle = '[data-toggle="dropdown"]' + , Dropdown = function ( element ) { + var $el = $(element).on('click.dropdown.data-api', this.toggle) + $('html').on('click.dropdown.data-api', function () { + $el.parent().removeClass('open') + }) + } + + Dropdown.prototype = { + + constructor: Dropdown + + , toggle: function ( e ) { + var $this = $(this) + , selector = $this.attr('data-target') + , $parent + , isActive + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + $parent.length || ($parent = $this.parent()) + + isActive = $parent.hasClass('open') + + clearMenus() + !isActive && $parent.toggleClass('open') + + return false + } + + } + + function clearMenus() { + $(toggle).parent().removeClass('open') + } + + + /* DROPDOWN PLUGIN DEFINITION + * ========================== */ + + $.fn.dropdown = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('dropdown') + if (!data) $this.data('dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.dropdown.Constructor = Dropdown + + + /* APPLY TO STANDARD DROPDOWN ELEMENTS + * =================================== */ + + $(function () { + $('html').on('click.dropdown.data-api', clearMenus) + $('body').on('click.dropdown.data-api', toggle, Dropdown.prototype.toggle) + }) + +}( window.jQuery ); \ No newline at end of file diff --git a/public/javascripts/vendor/jquery.iframe-transport.js b/public/javascripts/vendor/jquery.iframe-transport.js new file mode 100644 index 0000000000000000000000000000000000000000..3407da0a539ea94b1995c889e094ee79cc0d9528 --- /dev/null +++ b/public/javascripts/vendor/jquery.iframe-transport.js @@ -0,0 +1,233 @@ +// This [jQuery](http://jquery.com/) plugin implements an `<iframe>` +// [transport](http://api.jquery.com/extending-ajax/#Transports) so that +// `$.ajax()` calls support the uploading of files using standard HTML file +// input fields. This is done by switching the exchange from `XMLHttpRequest` to +// a hidden `iframe` element containing a form that is submitted. + +// The [source for the plugin](http://github.com/cmlenz/jquery-iframe-transport) +// is available on [Github](http://github.com/) and dual licensed under the MIT +// or GPL Version 2 licenses. + +// ## Usage + +// To use this plugin, you simply add a `iframe` option with the value `true` +// to the Ajax settings an `$.ajax()` call, and specify the file fields to +// include in the submssion using the `files` option, which can be a selector, +// jQuery object, or a list of DOM elements containing one or more +// `<input type="file">` elements: + +// $("#myform").submit(function() { +// $.ajax(this.action, { +// files: $(":file", this), +// iframe: true +// }).complete(function(data) { +// console.log(data); +// }); +// }); + +// The plugin will construct a hidden `<iframe>` element containing a copy of +// the form the file field belongs to, will disable any form fields not +// explicitly included, submit that form, and process the response. + +// If you want to include other form fields in the form submission, include them +// in the `data` option, and set the `processData` option to `false`: + +// $("#myform").submit(function() { +// $.ajax(this.action, { +// data: $(":text", this).serializeArray(), +// files: $(":file", this), +// iframe: true, +// processData: false +// }).complete(function(data) { +// console.log(data); +// }); +// }); + +// ### The Server Side + +// If the response is not HTML or XML, you (unfortunately) need to apply some +// trickery on the server side. To send back a JSON payload, send back an HTML +// `<textarea>` element with a `data-type` attribute that contains the MIME +// type, and put the actual payload in the textarea: + +// <textarea data-type="application/json"> +// {"ok": true, "message": "Thanks so much"} +// </textarea> + +// The iframe transport plugin will detect this and attempt to apply the same +// conversions that jQuery applies to regular responses. That means for the +// example above you should get a Javascript object as the `data` parameter of +// the `complete` callback, with the properties `ok: true` and +// `message: "Thanks so much"`. + +// ### Compatibility + +// This plugin has primarily been tested on Safari 5, Firefox 4, and Internet +// Explorer all the way back to version 6. While I haven't found any issues with +// it so far, I'm fairly sure it still doesn't work around all the quirks in all +// different browsers. But the code is still pretty simple overall, so you +// should be able to fix it and contribute a patch :) + +// ## Annotated Source + +(function($, undefined) { + + // Register a prefilter that checks whether the `iframe` option is set, and + // switches to the iframe transport if it is `true`. + $.ajaxPrefilter(function(options, origOptions, jqXHR) { + if (options.iframe) { + return "iframe"; + } + }); + + // Register an iframe transport, independent of requested data type. It will + // only activate when the "files" option has been set to a non-empty list of + // enabled file inputs. + $.ajaxTransport("iframe", function(options, origOptions, jqXHR) { + var form = null, + iframe = null, + origAction = null, + origTarget = null, + origEnctype = null, + addedFields = [], + disabledFields = [], + files = $(options.files).filter(":file:enabled"); + + // This function gets called after a successful submission or an abortion + // and should revert all changes made to the page to enable the + // submission via this transport. + function cleanUp() { + $(addedFields).each(function() { + this.remove(); + }); + $(disabledFields).each(function() { + this.disabled = false; + }); + form.attr("action", origAction || "") + .attr("target", origTarget || "") + .attr("enctype", origEnctype || ""); + iframe.attr("src", "javascript:false;").remove(); + } + + // Remove "iframe" from the data types list so that further processing is + // based on the content type returned by the server, without attempting an + // (unsupported) conversion from "iframe" to the actual type. + options.dataTypes.shift(); + + if (files.length) { + // Determine the form the file fields belong to, and make sure they all + // actually belong to the same form. + files.each(function() { + if (form !== null && this.form !== form) { + jQuery.error("All file fields must belong to the same form"); + } + form = this.form; + }); + form = $(form); + + // Store the original form attributes that we'll be replacing temporarily. + origAction = form.attr("action"); + origTarget = form.attr("target"); + origEnctype = form.attr("enctype"); + + // We need to disable all other inputs in the form so that they don't get + // included in the submitted data unexpectedly. + form.find(":input:not(:submit)").each(function() { + if (!this.disabled && (this.type != "file" || files.index(this) < 0)) { + this.disabled = true; + disabledFields.push(this); + } + }); + + // If there is any additional data specified via the `data` option, + // we add it as hidden fields to the form. This (currently) requires + // the `processData` option to be set to false so that the data doesn't + // get serialized to a string. + if (typeof(options.data) === "string" && options.data.length > 0) { + jQuery.error("data must not be serialized"); + } + $.each(options.data || {}, function(name, value) { + if ($.isPlainObject(value)) { + name = value.name; + value = value.value; + } + addedFields.push($("<input type='hidden'>").attr("name", name) + .attr("value", value).appendTo(form)); + }); + + // Add a hidden `X-Requested-With` field with the value `IFrame` to the + // field, to help server-side code to determine that the upload happened + // through this transport. + addedFields.push($("<input type='hidden' name='X-Requested-With'>") + .attr("value", "IFrame").appendTo(form)); + + // Borrowed straight from the JQuery source + // Provides a way of specifying the accepted data type similar to HTTP_ACCEPTS + accepts = options.dataTypes[ 0 ] && options.accepts[ options.dataTypes[0] ] ? + options.accepts[ options.dataTypes[0] ] + ( options.dataTypes[ 0 ] !== "*" ? ", */*; q=0.01" : "" ) : + options.accepts[ "*" ] + + addedFields.push($("<input type='hidden' name='X-Http-Accept'>") + .attr("value", accepts).appendTo(form)); + + return { + + // The `send` function is called by jQuery when the request should be + // sent. + send: function(headers, completeCallback) { + iframe = $("<iframe src='javascript:false;' name='iframe-" + $.now() + + "' style='display:none'></iframe>"); + + // The first load event gets fired after the iframe has been injected + // into the DOM, and is used to prepare the actual submission. + iframe.bind("load", function() { + + // The second load event gets fired when the response to the form + // submission is received. The implementation detects whether the + // actual payload is embedded in a `<textarea>` element, and + // prepares the required conversions to be made in that case. + iframe.unbind("load").bind("load", function() { + + var doc = this.contentWindow ? this.contentWindow.document : + (this.contentDocument ? this.contentDocument : this.document), + root = doc.documentElement ? doc.documentElement : doc.body, + textarea = root.getElementsByTagName("textarea")[0], + type = textarea ? textarea.getAttribute("data-type") : null; + + var status = textarea ? parseInt(textarea.getAttribute("response-code")) : 200, + statusText = "OK", + responses = { text: type ? textarea.value : root ? root.innerHTML : null }, + headers = "Content-Type: " + (type || "text/html") + + completeCallback(status, statusText, responses, headers); + + setTimeout(cleanUp, 50); + }); + + // Now that the load handler has been set up, reconfigure and + // submit the form. + form.attr("action", options.url) + .attr("target", iframe.attr("name")) + .attr("enctype", "multipart/form-data") + .get(0).submit(); + }); + + // After everything has been set up correctly, the iframe gets + // injected into the DOM so that the submission can be initiated. + iframe.insertAfter(form); + }, + + // The `abort` function is called by jQuery when the request should be + // aborted. + abort: function() { + if (iframe !== null) { + iframe.unbind("load").attr("src", "javascript:false;"); + cleanUp(); + } + } + + }; + } + }); + +})(jQuery); diff --git a/public/javascripts/vendor/jquery.remotipart.js b/public/javascripts/vendor/jquery.remotipart.js new file mode 100644 index 0000000000000000000000000000000000000000..9e00854e00937bf108c2ad7e730e83bf90e70da5 --- /dev/null +++ b/public/javascripts/vendor/jquery.remotipart.js @@ -0,0 +1,69 @@ +//= require jquery.iframe-transport.js +//= require_self + +(function($) { + + var remotipart; + + $.remotipart = remotipart = { + + setup: function(form) { + form + // Allow setup part of $.rails.handleRemote to setup remote settings before canceling default remote handler + // This is required in order to change the remote settings using the form details + .one('ajax:beforeSend.remotipart', function(e, xhr, settings){ + // Delete the beforeSend bindings, since we're about to re-submit via ajaxSubmit with the beforeSubmit + // hook that was just setup and triggered via the default `$.rails.handleRemote` + // delete settings.beforeSend; + delete settings.beforeSend; + + settings.iframe = true; + settings.files = $($.rails.fileInputSelector, form); + settings.data = form.serializeArray(); + settings.processData = false; + + // Modify some settings to integrate JS request with rails helpers and middleware + if (settings.dataType === undefined) { settings.dataType = 'script *'; } + settings.data.push({name: 'remotipart_submitted', value: true}); + + // Allow remotipartSubmit to be cancelled if needed + if ($.rails.fire(form, 'ajax:remotipartSubmit', [xhr, settings])) { + // Second verse, same as the first + $.rails.ajax(settings); + } + + //Run cleanup + remotipart.teardown(form); + + // Cancel the jQuery UJS request + return false; + }) + + // Keep track that we just set this particular form with Remotipart bindings + // Note: The `true` value will get over-written with the `settings.dataType` from the `ajax:beforeSend` handler + .data('remotipartSubmitted', true); + }, + + teardown: function(form) { + form + .unbind('ajax:beforeSend.remotipart') + .removeData('remotipartSubmitted') + } + }; + + $('form').live('ajax:aborted:file', function(){ + var form = $(this); + + remotipart.setup(form); + + // If browser does not support submit bubbling, then this live-binding will be called before direct + // bindings. Therefore, we should directly call any direct bindings before remotely submitting form. + if (!$.support.submitBubbles && $().jquery < '1.7' && $.rails.callFormSubmitBindings(form) === false) return $.rails.stopEverything(e); + + // Manually call jquery-ujs remote call so that it can setup form and settings as usual, + // and trigger the `ajax:beforeSend` callback to which remotipart binds functionality. + $.rails.handleRemote(form); + return false; + }); + +})(jQuery); diff --git a/public/javascripts/view.js b/public/javascripts/view.js index 39543a2cb5aa15c8f02057f328fe51e764b63312..977b7cded8005bdf7b8e7b03190bad6cc9a45786 100644 --- a/public/javascripts/view.js +++ b/public/javascripts/view.js @@ -25,7 +25,25 @@ var View = { /* Avatars */ $(this.avatars.selector).error(this.avatars.fallback); - /* Clear forms after successful submit */ + /* Clear forms after successful submit, this is some legacy dan hanson stuff, do we still want it? */ + $.fn.clearForm = function() { + return this.each(function() { + if ($(this).is('form')) { + return $(':input', this).clearForm(); + } + if ($(this).hasClass('clear_on_submit') || $(this).is(':text') || $(this).is(':password') || $(this).is('textarea')) { + $(this).val(''); + } else if ($(this).is(':checkbox') || $(this).is(':radio')) { + $(this).attr('checked', false); + } else if ($(this).is('select')) { + this.selectedIndex = -1; + } else if ($(this).attr('name') == 'photos[]') { + $(this).val(''); + } + $(this).blur(); + }); + }; + $('form[data-remote]').live('ajax:success', function (e) { $(this).clearForm(); $(this).focusout(); diff --git a/public/stylesheets/sass/new-templates.scss b/public/stylesheets/sass/new-templates.scss index 094627e99f6300e3d6419335f9403a5564e748a8..c8ce38355676c758d7ba4d14fba067c3c27e8367 100644 --- a/public/stylesheets/sass/new-templates.scss +++ b/public/stylesheets/sass/new-templates.scss @@ -226,7 +226,7 @@ $pane-width: 420px; .photo-fill { @include background-cover(); - + z-index : -5000; //so the framer controls don't get lost position: absolute; top: 0; left: 0; @@ -250,6 +250,7 @@ $pane-width: 420px; } .rich-media { + z-index : -5000; //so the framer controls don't get lost position: absolute; height: 100%; width: 100%; @@ -764,3 +765,47 @@ text-rendering: optimizelegibility; right: 8px; top: 8px; } + +.aspects_dropdown { + i { + display: none; + } + + .selected i { + display: inline-block; + } + + a { + display : inline-block; + } +} + +.new_photo .photo{ + display: inline; + max-width: 200px; + max-height: 200px; +} + +.new-post-section { + margin-top: 100px; +} + +.aspect_selector { + float: right; +} + +.post-view { + display: table; + height: 100%; + width: 100%; +} + +#post-content { + button { + position: absolute; + } +} + +.headline p{ + @include media-text(); +} \ No newline at end of file diff --git a/spec/controllers/status_messages_controller_spec.rb b/spec/controllers/status_messages_controller_spec.rb index b6dbd587de06e753d4b406094c95b02f56cb356c..fa823b3fd590f447f02cd365b73cbc85b222cdcf 100644 --- a/spec/controllers/status_messages_controller_spec.rb +++ b/spec/controllers/status_messages_controller_spec.rb @@ -75,7 +75,7 @@ describe StatusMessagesController do { :status_message => { :public => "true", :text => "facebook, is that you?", - }, + }, :aspect_ids => [@aspect1.id.to_s] } } diff --git a/spec/javascripts/app/app_spec.js b/spec/javascripts/app/app_spec.js index 6989301f41a04ad33d77ca76050329db50fa1443..9323020d94dd2835b9c4f3e624ae013b4d2af37d 100644 --- a/spec/javascripts/app/app_spec.js +++ b/spec/javascripts/app/app_spec.js @@ -3,20 +3,16 @@ describe("app", function() { it("sets the user if given one and returns the current user", function() { expect(app.user()).toBeFalsy() }); - + it("returns false if the current_user isn't set", function() { app._user = undefined; - expect(app.user()).toEqual(false); }); - }); - describe('currentUser', function(){ it("sets the user if given one and returns the current user", function() { - expect(app.currentUser.authenticated()).toBeFalsy() + expect(app.user()).toBeFalsy() app.user({name: "alice"}); - expect(app.user().get("name")).toEqual("alice"); }); }); -}); +}) diff --git a/spec/javascripts/app/forms/picture_form_spec.js b/spec/javascripts/app/forms/picture_form_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..42d53cce1e5f87e86a5a99d713d54711068a3c67 --- /dev/null +++ b/spec/javascripts/app/forms/picture_form_spec.js @@ -0,0 +1,57 @@ +describe("app.forms.Picture", function(){ + beforeEach(function(){ + $("<meta/>", { + "name" : "csrf-token", + "content" : "supersecrettokenlol" + }).prependTo("head") + + this.form = new app.forms.Picture().render() + }); + + it("sets the authenticity token from the meta tag", function(){ + expect(this.form.$("input[name='authenticity_token']").val()).toBe("supersecrettokenlol") + }); + + describe("selecting a photo", function(){ + it("submits the form", function(){ + var submitSpy = jasmine.createSpy(); + + this.form.$("form").submit(function(event){ + event.preventDefault(); + submitSpy(); + }); + + this.form.$("input[name='photo[user_file]']").change() + expect(submitSpy).toHaveBeenCalled(); + }) + }); + + describe("when a photo is suceessfully submitted", function(){ + beforeEach(function(){ + this.photoAttrs = { name : "Obama rides a bicycle" } + this.respond = function() { + this.form.$(".new_photo").trigger("ajax:complete", { + responseText : JSON.stringify({success : true, data : this.photoAttrs}) + }) + } + }) + + it("adds a new model to the photos", function(){ + expect(this.form.$(".photos div").length).toBe(0); + this.respond() + expect(this.form.$(".photos div").length).toBeGreaterThan(0); + }) + }) + + describe("when a photo is unsuccessfully submitted", function(){ + beforeEach(function(){ + this.response = {responseText : JSON.stringify({success : false, message : "I like to eat basketballs"}) } + }) + + it("adds a new model to the photos", function(){ + spyOn(window, "alert") + this.form.$(".new_photo").trigger("ajax:complete", this.response) + expect(window.alert).toHaveBeenCalled(); + }) + }) +}); \ No newline at end of file diff --git a/spec/javascripts/app/forms/post_form_spec.js b/spec/javascripts/app/forms/post_form_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..8514e1db2471597d7b80d665083aa478bd446061 --- /dev/null +++ b/spec/javascripts/app/forms/post_form_spec.js @@ -0,0 +1,42 @@ +describe("app.forms.Post", function(){ + beforeEach(function(){ + this.post = new app.models.Post(); + this.view = new app.forms.Post({model : this.post}) + }) + + describe("rendering", function(){ + beforeEach(function(){ + this.view.render() + }) + + + describe("submitting a valid form", function(){ + beforeEach(function(){ + this.view.$("form #text_with_markup").val("Oh My") + this.view.$("form .aspect_ids").val("public") + + /* appending checkboxes */ + this.view.$(".new-post").append($("<input/>", { + value : "fakeBook", + checked : "checked", + "class" : "service", + "type" : "checkbox" + })) + + this.view.$(".new-post").append($("<input/>", { + value : "twitter", + checked : "checked", + "class" : "service", + "type" : "checkbox" + })) + }) + + it("instantiates a post on form submit", function(){ + this.view.$(".new-post").submit() + expect(this.view.model.get("text")).toBe("Oh My") + expect(this.view.model.get("aspect_ids")).toBe("public") + expect(this.view.model.get("services").length).toBe(2) + }) + }) + }) +}) diff --git a/spec/javascripts/app/models/status_message_spec.js b/spec/javascripts/app/models/status_message_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..ad8c93a31ec1bbad84d1fd9171c73bd8dda1a98f --- /dev/null +++ b/spec/javascripts/app/models/status_message_spec.js @@ -0,0 +1,12 @@ +describe("app.models.StatusMessage", function(){ + describe("#url", function(){ + it("is /status_messages when its new", function(){ + var post = new app.models.StatusMessage() + expect(post.url()).toBe("/status_messages") + }) + + it("is /posts/id when it has an id", function(){ + expect(new app.models.StatusMessage({id : 5}).url()).toBe("/posts/5") + }) + }) +}) \ No newline at end of file diff --git a/spec/javascripts/app/pages/framer_spec.js b/spec/javascripts/app/pages/framer_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..db9deb0de45d058eb59a1eaf8f463be53b394083 --- /dev/null +++ b/spec/javascripts/app/pages/framer_spec.js @@ -0,0 +1,27 @@ +describe("app.pages.Framer", function(){ + beforeEach(function(){ + loginAs(factory.user()) + app.frame = new factory.statusMessage(); + this.page = new app.pages.Framer(); + }); + + it("passes the model down to the template picker", function(){ + expect(this.page.templatePicker.model).toBe(app.frame) + }); + + it("passes the model down to the post view", function(){ + expect(this.page.postView().model).toBe(app.frame) + }); + + describe("rendering", function(){ + beforeEach(function(){ + this.page.render(); + }); + + it("saves the model when you click done",function(){ + spyOn(app.frame, "save"); + this.page.$("button.done").click(); + expect(app.frame.save).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/app/pages/post_new_spec.js b/spec/javascripts/app/pages/post_new_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..0c701e51fb8ea570f6bf868fb7c7aa8a9c9fb847 --- /dev/null +++ b/spec/javascripts/app/pages/post_new_spec.js @@ -0,0 +1,32 @@ +describe("app.pages.PostNew", function(){ + beforeEach(function(){ + this.page = new app.pages.PostNew() + }) + + describe("rendering", function(){ + beforeEach(function(){ + this.page.render(); + }) + + describe("clicking next", function(){ + beforeEach(function(){ + spyOn(app.router, "navigate") + spyOn(this.page.postForm, "setModelAttributes") + this.page.$("button.next").click() + }) + + it("calls tells the form to set the models attributes", function(){ + expect(this.page.postForm.setModelAttributes).toHaveBeenCalled(); + }); + + it("stores a reference to the form as app.composer" , function(){ + expect(this.page.model).toBeDefined() + expect(app.frame).toBe(this.page.model) + }); + + it("navigates to the framer", function(){ + expect(app.router.navigate).toHaveBeenCalledWith("framer", true) + }); + }) + }) +}); \ No newline at end of file diff --git a/spec/javascripts/app/views/aspects_dropdown_view_spec.js b/spec/javascripts/app/views/aspects_dropdown_view_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..b5a47604d642849ec231c1452d7a9bb1ba5d2378 --- /dev/null +++ b/spec/javascripts/app/views/aspects_dropdown_view_spec.js @@ -0,0 +1,114 @@ +describe("app.views.AspectsDropdown", function(){ + beforeEach(function(){ + loginAs(factory.user({ + aspects : [ + { id : 3, name : "sauce" }, + { id : 5, name : "conf" }, + { id : 7, name : "lovers" } + ] + })) + + this.view = new app.views.AspectsDropdown + }) + + describe("rendering", function(){ + beforeEach(function(){ + this.view.render() + }) + + it("defaults to All Aspects Visibility", function(){ + expect(this.view.$("input.aspect_ids").val()).toBe("all_aspects") + expect($.trim(this.view.$(".dropdown-toggle .text").text())).toBe("All Aspects") + }) + + describe("selecting Public", function(){ + beforeEach(function(){ + this.link = this.view.$("a[data-visibility='public']") + this.link.click() + }) + + it("calls set aspect_ids to 'public'", function(){ + expect(this.view.$("input.aspect_ids").val()).toBe("public") + }) + + it("sets the dropdown title to 'public'", function(){ + expect(this.view.$(".dropdown-toggle .text").text()).toBe("Public") + }) + + it("adds the selected class to the link", function(){ + expect(this.link.parent().hasClass("selected")).toBeTruthy(); + }) + }) + + describe("selecting All Aspects", function(){ + beforeEach(function(){ + this.link = this.view.$("a[data-visibility='all-aspects']") + this.link.click() + }) + + it("calls set aspect_ids to 'all'", function(){ + expect(this.view.$("input.aspect_ids").val()).toBe("all_aspects") + }) + + it("sets the dropdown title to 'public'", function(){ + expect($.trim(this.view.$(".dropdown-toggle .text").text())).toBe("All Aspects") + }) + + it("adds the selected class to the link", function(){ + expect(this.link.parent().hasClass("selected")).toBeTruthy(); + }) + }) + + + describe("selecting An Aspect", function(){ + beforeEach(function(){ + this.link = this.view.$("a:contains('lovers')") + this.link.click() + }) + + it("sets the dropdown title to the aspect title", function(){ + expect($.trim(this.view.$(".dropdown-toggle .text").text())).toBe("lovers") + }) + + it("adds the selected class to the link", function(){ + expect(this.link.parent().hasClass("selected")).toBeTruthy(); + }) + + it("sets aspect_ids to to the aspect id", function(){ + expect(this.view.$("input.aspect_ids").val()).toBe("7") + }) + + describe("selecting another aspect", function(){ + beforeEach(function(){ + this.view.$("a:contains('sauce')").click() + }) + + it("sets aspect_ids to the selected aspects", function(){ + expect(this.view.$("input.aspect_ids").val()).toBe("3,7") + }) + + describe("deselecting another aspect", function(){ + it("removes the clicked aspect", function(){ + expect(this.view.$("input.aspect_ids").val()).toBe("3,7") + this.view.$("a:contains('lovers')").click() + expect(this.view.$("input.aspect_ids").val()).toBe("3") + }) + }) + + describe("selecting all_aspects", function(){ + it("sets aspect_ids to all_aspects", function(){ + this.view.$("a[data-visibility='all-aspects']").click() + expect(this.view.$("input.aspect_ids").val()).toBe("all_aspects") + }) + }) + + describe("selecting public", function(){ + it("sets aspect_ids to public", function(){ + this.view.$("a[data-visibility='public']").click() + expect(this.view.$("input.aspect_ids").val()).toBe("public") + }) + }) + }) + }) + }) +}) \ No newline at end of file diff --git a/spec/javascripts/app/views/photo_viewer_spec.js b/spec/javascripts/app/views/photo_viewer_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..6f25d580b6b646912a491f81a12a1ad26ad7d202 --- /dev/null +++ b/spec/javascripts/app/views/photo_viewer_spec.js @@ -0,0 +1,19 @@ +describe("app.views.PhotoViewer", function(){ + beforeEach(function(){ + this.model = factory.post({ + photos : [ + factory.photoAttrs({sizes : {large : "http://tieguy.org/me.jpg"}}), + factory.photoAttrs({sizes : {large : "http://whatthefuckiselizabethstarkupto.com/none_knows.gif"}}) //SIC + ] + }) + this.view = new app.views.PhotoViewer({model : this.model}) + }) + + describe("rendering", function(){ + it("should have an image for each photoAttr on the model", function(){ + this.view.render() + expect(this.view.$("img").length).toBe(2) + expect(this.view.$("img[src='http://tieguy.org/me.jpg']")).toExist() + }) + }) +}) \ No newline at end of file diff --git a/spec/javascripts/app/views/post/day_view_spec.js b/spec/javascripts/app/views/post/day_view_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..397ac46151b9857a7a90a6b2c86f0d7d2206ae94 --- /dev/null +++ b/spec/javascripts/app/views/post/day_view_spec.js @@ -0,0 +1,34 @@ +describe("app.views.Post.Day", function(){ + beforeEach(function(){ + this.post = factory.post() + this.view = new app.views.Post.Day({model : this.post}) + }) + + describe("rendering", function(){ + it("is happy", function(){ + this.view.render() + }) + + describe("when the text is under 140 characters", function(){ + it("has class headline", function(){ + this.post.set({text : "Lol this is a short headline"}) + this.view.render() + expect(this.view.$("section.text")).toHaveClass("headline") + }) + }) + + describe("when the text is over 140 characters", function(){ + it("has doesn't have headline", function(){ + this.post.set({text :"Vegan bushwick tempor labore. Nulla seitan anim, aesthetic ex gluten-free viral" + + "thundercats street art. Occaecat carles deserunt lomo messenger bag wes anderson. Narwhal cray selvage " + + "dolor. Mixtape wes anderson american apparel, mustache readymade cred nulla squid veniam small batch id " + + "cupidatat. Pork belly high life consequat, raw denim sint terry richardson seitan single-origin coffee " + + "butcher. Sint yr fugiat cillum." + }) + + this.view.render() + expect(this.view.$("section.text")).not.toHaveClass("headline") + }) + }) + }) +}) \ No newline at end of file diff --git a/spec/javascripts/app/views/post_view_spec.js b/spec/javascripts/app/views/post_view_spec.js index d1ef0e2b35da511db74c884b5ca36ccecfb7ad4f..58225d1afbfffc37e6f19188bd3ac9dcd64c533b 100644 --- a/spec/javascripts/app/views/post_view_spec.js +++ b/spec/javascripts/app/views/post_view_spec.js @@ -1,188 +1,3 @@ describe("app.views.Post", function(){ - - describe("#render", function(){ - beforeEach(function(){ - loginAs({name: "alice", avatar : {small : "http://avatar.com/photo.jpg"}}); - - Diaspora.I18n.loadLocale({stream : { - reshares : { - one : "<%= count %> reshare", - other : "<%= count %> reshares" - }, - likes : { - zero : "<%= count %> Likes", - one : "<%= count %> Like", - other : "<%= count %> Likes" - } - }}) - - var posts = $.parseJSON(spec.readFixture("stream_json"))["posts"]; - - this.collection = new app.collections.Posts(posts); - this.statusMessage = this.collection.models[0]; - this.reshare = this.collection.models[1]; - }) - - context("reshare", function(){ - it("displays a reshare count", function(){ - this.statusMessage.set({reshares_count : 2}) - var view = new app.views.Post({model : this.statusMessage}).render(); - - expect($(view.el).html()).toContain(Diaspora.I18n.t('stream.reshares', {count: 2})) - }) - - it("does not display a reshare count for 'zero'", function(){ - this.statusMessage.set({reshares_count : 0}) - var view = new app.views.Post({model : this.statusMessage}).render(); - - expect($(view.el).html()).not.toContain("0 Reshares") - }) - }) - - context("likes", function(){ - it("displays a like count", function(){ - this.statusMessage.set({likes_count : 1}) - var view = new app.views.Post({model : this.statusMessage}).render(); - - expect($(view.el).html()).toContain(Diaspora.I18n.t('stream.likes', {count: 1})) - }) - it("does not display a like count for 'zero'", function(){ - this.statusMessage.set({likes_count : 0}) - var view = new app.views.Post({model : this.statusMessage}).render(); - - expect($(view.el).html()).not.toContain("0 Likes") - }) - }) - - context("embed_html", function(){ - it("provides oembed html from the model response", function(){ - this.statusMessage.set({"o_embed_cache" : { - "data" : { - "html" : "some html" - } - }}) - - var view = new app.views.Content({model : this.statusMessage}); - expect(view.presenter().o_embed_html).toContain("some html") - }) - - it("does not provide oembed html from the model response if none is present", function(){ - this.statusMessage.set({"o_embed_cache" : null}) - - var view = new app.views.Content({model : this.statusMessage}); - expect(view.presenter().o_embed_html).toBe(""); - }) - }) - - context("user not signed in", function(){ - it("does not provide a Feedback view", function(){ - logout() - var view = new app.views.Post({model : this.statusMessage}).render(); - expect(view.feedbackView()).toBeFalsy(); - }) - }) - - context("NSFW", function(){ - beforeEach(function(){ - this.statusMessage.set({nsfw: true}); - this.view = new app.views.Post({model : this.statusMessage}).render(); - - this.hiddenPosts = function(){ - return this.view.$(".nsfw-shield") - } - }); - - it("contains a shield element", function(){ - expect(this.hiddenPosts().length).toBe(1) - }); - - it("does not contain a shield element when nsfw is false", function(){ - this.statusMessage.set({nsfw: false}); - this.view.render(); - expect(this.hiddenPosts()).not.toExist(); - }) - - context("showing a single post", function(){ - it("removes the shields when the post is clicked", function(){ - expect(this.hiddenPosts()).toExist(); - this.view.$(".nsfw-shield .show_nsfw_post").click(); - expect(this.hiddenPosts()).not.toExist(); - }); - }); - - context("clicking the toggle nsfw link toggles it on the user", function(){ - it("calls toggleNsfw on the user", function(){ - spyOn(app.user(), "toggleNsfwState") - this.view.$(".toggle_nsfw_state").first().click(); - expect(app.user().toggleNsfwState).toHaveBeenCalled(); - }); - }) - }) - - context("user views their own post", function(){ - beforeEach(function(){ - this.statusMessage.set({ author: { - id : app.user().id - }}); - this.view = new app.views.Post({model : this.statusMessage}).render(); - }) - - it("contains remove post", function(){ - expect(this.view.$(".remove_post")).toExist(); - }) - - it("destroys the view when they delete a their post from the show page", function(){ - spyOn(window, "confirm").andReturn(true); - - this.view.$(".remove_post").click(); - - expect(window.confirm).toHaveBeenCalled(); - expect(this.view).not.toExist(); - }) - }) - - context("markdown rendering", function() { - beforeEach(function() { - // example from issue #2665 - this.evilUrl = "http://www.bürgerentscheid-krankenhäuser.de"; - this.asciiUrl = "http://www.xn--brgerentscheid-krankenhuser-xkc78d.de"; - }); - - it("correctly handles non-ascii characters in urls", function() { - this.statusMessage.set({text: "<"+this.evilUrl+">"}); - var view = new app.views.Post({model : this.statusMessage}).render(); - - expect($(view.el).html()).toContain(this.asciiUrl); - expect($(view.el).html()).toContain(this.evilUrl); - }); - - it("doesn't break link texts for non-ascii urls", function() { - var linkText = "check out this awesome link!"; - this.statusMessage.set({text: "["+linkText+"]("+this.evilUrl+")"}); - var view = new app.views.Post({model: this.statusMessage}).render(); - - expect($(view.el).html()).toContain(this.asciiUrl); - expect($(view.el).html()).toContain(linkText); - }); - - it("doesn't break reference style links for non-ascii urls", function() { - var postContent = "blabla blab [my special link][1] bla blabla\n\n[1]: "+this.evilUrl+" and an optional title)"; - this.statusMessage.set({text: postContent}); - var view = new app.views.Post({model: this.statusMessage}).render(); - - expect($(view.el).html()).not.toContain(this.evilUrl); - expect($(view.el).html()).toContain(this.asciiUrl); - }); - - it("correctly handles images with non-ascii urls", function() { - var postContent = ""; - var niceImg = '"http://xn--bndnis-fr-krankenhuser-i5b27cha.de/wp-content/uploads/2011/11/cropped-logohp.jpg"'; - this.statusMessage.set({text: postContent}); - var view = new app.views.Post({model: this.statusMessage}).render(); - - expect($(view.el).html()).toContain(niceImg); - }); - - }); - }) -}); + //check out StreamPost +}) diff --git a/spec/javascripts/app/views/services_selector_view_spec.js b/spec/javascripts/app/views/services_selector_view_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..afa19486a578339921c68b8f4c6b25f3c40d508f --- /dev/null +++ b/spec/javascripts/app/views/services_selector_view_spec.js @@ -0,0 +1,23 @@ +describe("app.views.ServicesSelector", function(){ + beforeEach(function(){ + loginAs(factory.user({ + services : [ + { provider : "fakeBook" } + ] + })); + + this.view = new app.views.ServicesSelector(); + }); + + describe("rendering", function(){ + beforeEach(function(){ + this.view.render(); + }); + + it("displays all services", function(){ + var checkboxes = $(this.view.el).find('input[type="checkbox"]'); + + expect(checkboxes.val()).toBe("fakeBook"); + }); + }); +}); \ No newline at end of file diff --git a/spec/javascripts/app/views/stream_post_spec.js b/spec/javascripts/app/views/stream_post_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..7b0f545f67e86393292ef71127ea2684ca99f6fe --- /dev/null +++ b/spec/javascripts/app/views/stream_post_spec.js @@ -0,0 +1,148 @@ +describe("app.views.StreamPost", function(){ + beforeEach(function(){ + this.PostViewClass = app.views.StreamPost + }) + + describe("#render", function(){ + beforeEach(function(){ + loginAs({name: "alice", avatar : {small : "http://avatar.com/photo.jpg"}}); + + Diaspora.I18n.loadLocale({stream : { + reshares : { + one : "<%= count %> reshare", + other : "<%= count %> reshares" + }, + likes : { + zero : "<%= count %> Likes", + one : "<%= count %> Like", + other : "<%= count %> Likes" + } + }}) + + var posts = $.parseJSON(spec.readFixture("stream_json"))["posts"]; + + this.collection = new app.collections.Posts(posts); + this.statusMessage = this.collection.models[0]; + this.reshare = this.collection.models[1]; + }) + + context("reshare", function(){ + it("displays a reshare count", function(){ + this.statusMessage.set({reshares_count : 2}) + var view = new this.PostViewClass({model : this.statusMessage}).render(); + + expect($(view.el).html()).toContain(Diaspora.I18n.t('stream.reshares', {count: 2})) + }) + + it("does not display a reshare count for 'zero'", function(){ + this.statusMessage.set({reshares_count : 0}) + var view = new this.PostViewClass({model : this.statusMessage}).render(); + + expect($(view.el).html()).not.toContain("0 Reshares") + }) + }) + + context("likes", function(){ + it("displays a like count", function(){ + this.statusMessage.set({likes_count : 1}) + var view = new this.PostViewClass({model : this.statusMessage}).render(); + + expect($(view.el).html()).toContain(Diaspora.I18n.t('stream.likes', {count: 1})) + }) + it("does not display a like count for 'zero'", function(){ + this.statusMessage.set({likes_count : 0}) + var view = new this.PostViewClass({model : this.statusMessage}).render(); + + expect($(view.el).html()).not.toContain("0 Likes") + }) + }) + + context("embed_html", function(){ + it("provides oembed html from the model response", function(){ + this.statusMessage.set({"o_embed_cache" : { + "data" : { + "html" : "some html" + } + }}) + + var view = new app.views.Content({model : this.statusMessage}); + expect(view.presenter().o_embed_html).toContain("some html") + }) + + it("does not provide oembed html from the model response if none is present", function(){ + this.statusMessage.set({"o_embed_cache" : null}) + + var view = new app.views.Content({model : this.statusMessage}); + expect(view.presenter().o_embed_html).toBe(""); + }) + }) + + context("user not signed in", function(){ + it("does not provide a Feedback view", function(){ + logout() + var view = new this.PostViewClass({model : this.statusMessage}).render(); + expect(view.feedbackView()).toBeFalsy(); + }) + }) + + context("NSFW", function(){ + beforeEach(function(){ + this.statusMessage.set({nsfw: true}); + this.view = new this.PostViewClass({model : this.statusMessage}).render(); + + this.hiddenPosts = function(){ + return this.view.$(".nsfw-shield") + } + }); + + it("contains a shield element", function(){ + expect(this.hiddenPosts().length).toBe(1) + }); + + it("does not contain a shield element when nsfw is false", function(){ + this.statusMessage.set({nsfw: false}); + this.view.render(); + expect(this.hiddenPosts()).not.toExist(); + }) + + context("showing a single post", function(){ + it("removes the shields when the post is clicked", function(){ + expect(this.hiddenPosts()).toExist(); + this.view.$(".nsfw-shield .show_nsfw_post").click(); + expect(this.hiddenPosts()).not.toExist(); + }); + }); + + context("clicking the toggle nsfw link toggles it on the user", function(){ + it("calls toggleNsfw on the user", function(){ + spyOn(app.user(), "toggleNsfwState") + this.view.$(".toggle_nsfw_state").first().click(); + expect(app.user().toggleNsfwState).toHaveBeenCalled(); + }); + }) + }) + + context("user views their own post", function(){ + beforeEach(function(){ + this.statusMessage.set({ author: { + id : app.user().id + }}); + this.view = new this.PostViewClass({model : this.statusMessage}).render(); + }) + + it("contains remove post", function(){ + expect(this.view.$(".remove_post")).toExist(); + }) + + it("destroys the view when they delete a their post from the show page", function(){ + spyOn(window, "confirm").andReturn(true); + + this.view.$(".remove_post").click(); + + expect(window.confirm).toHaveBeenCalled(); + expect(this.view).not.toExist(); + }) + }) + + }) +}); diff --git a/spec/javascripts/app/views/template_picker_view_spec.js b/spec/javascripts/app/views/template_picker_view_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..7b1e8ef19b92c0c93a1aea16c35c0987db1e624b --- /dev/null +++ b/spec/javascripts/app/views/template_picker_view_spec.js @@ -0,0 +1,28 @@ +describe("app.views.TemplatePicker", function(){ + beforeEach(function(){ + this.model = factory.statusMessage({frame_name: undefined}) + this.view = new app.views.TemplatePicker({model : this.model }) + }) + + describe("initialization", function(){ + it("sets the frame_name of the model to 'status' by default", function(){ + expect(this.view.model.get("frame_name")).toBe("status") + }) + }) + + describe("rendering", function(){ + beforeEach(function(){ + this.view.render() + }) + + it("selects the model's frame_name from the dropdown", function(){ + expect(this.view.$("select[name=template]").val()).toBe("status") + }) + + it("changes the frame_name on the model when is is selected", function(){ + this.view.$("select[name=template]").val("note") + this.view.$("select[name=template]").trigger("change") + expect(this.model.get("frame_name")).toBe('note') + }) + }) +}) \ No newline at end of file diff --git a/spec/javascripts/helpers/factory.js b/spec/javascripts/helpers/factory.js index 03be9097c5a76de23db0b7badbe71a0caa57c3eb..b1ff242729f6eff4c9fc5074106b68026e36ee2a 100644 --- a/spec/javascripts/helpers/factory.js +++ b/spec/javascripts/helpers/factory.js @@ -33,6 +33,10 @@ factory = { return new app.models.Comment(_.extend(defaultAttrs, overrides)) }, + user : function(overrides) { + return new app.models.User(factory.userAttrs(overrides)) + }, + userAttrs : function(overrides){ var id = this.id.next() var defaultAttrs = { @@ -48,8 +52,8 @@ factory = { return _.extend(defaultAttrs, overrides) }, - post : function(overrides) { - var defaultAttrs = { + postAttrs : function(){ + return { "provider_display_name" : null, "created_at" : "2012-01-03T19:53:13Z", "interacted_at" : '2012-01-03T19:53:13Z', @@ -57,7 +61,6 @@ factory = { "public" : false, "guid" : this.guid(), "image_url" : null, - "author" : this.author(), "o_embed_cache" : null, "photos" : [], "text" : "jasmine is bomb", @@ -69,10 +72,32 @@ factory = { "likes_count" : 0, "comments_count" : 0 } + }, + photoAttrs : function(overrides){ + return _.extend({ + author: factory.userAttrs(), + created_at: "2012-03-27T20:11:52Z", + guid: "8b0db16a4c4307b2", + id: 117, + sizes: { + large: "http://localhost:3000/uploads/images/scaled_full_d85410bd19db1016894c.jpg", + medium: "http://localhost:3000/uploads/images/thumb_medium_d85410bd19db1016894c.jpg", + small: "http://localhost:3000/uploads/images/thumb_small_d85410bd19db1016894c.jpg" + } + }, overrides) + }, + + post : function(overrides) { + defaultAttrs = _.extend(factory.postAttrs(), {"author" : this.author()}) return new app.models.Post(_.extend(defaultAttrs, overrides)) }, + statusMessage : function(overrides){ + //intentionally doesn't have an author to mirror creation process, maybe we should change the creation process + return new app.models.StatusMessage(_.extend(factory.postAttrs(), overrides)) + }, + comment: function(overrides) { var defaultAttrs = { "text" : "This is an awesome comment!", diff --git a/spec/javascripts/support/jasmine.yml b/spec/javascripts/support/jasmine.yml index 83fbd18746b400064e1c23f26198310d2948de28..81f68950c3f4adeac06ce98bc0ac76b00ad02fec 100644 --- a/spec/javascripts/support/jasmine.yml +++ b/spec/javascripts/support/jasmine.yml @@ -18,8 +18,8 @@ src_files: - public/javascripts/vendor/underscore.js - public/javascripts/vendor/jquery-1.7.1.min.js - public/javascripts/vendor/jquery-ui-1.8.9.custom.min.js - - public/javascripts/vendor/bootstrap/bootstrap-popover.js - public/javascripts/vendor/bootstrap/bootstrap-twipsy.js + - public/javascripts/vendor/bootstrap/bootstrap-popover.js - public/javascripts/vendor/jquery.tipsy.js - public/javascripts/vendor/jquery.infinitescroll.min.js - public/javascripts/vendor/jquery.autoresize.js @@ -46,6 +46,7 @@ src_files: - public/javascripts/app/helpers/* - public/javascripts/app/router.js - public/javascripts/app/views.js + - public/javascripts/app/forms.js - public/javascripts/app/models/post.js - public/javascripts/app/models/* - public/javascripts/app/collections/* @@ -53,6 +54,8 @@ src_files: - public/javascripts/app/views/content_view.js - public/javascripts/app/views/*.js - public/javascripts/app/views/**/*.js + - public/javascripts/app/pages/**/*.js + - public/javascripts/app/forms/**/*.js - public/javascripts/mobile.js - public/javascripts/contact-list.js diff --git a/spec/presenters/aspect_presenter_spec.rb b/spec/presenters/aspect_presenter_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d38b97f426f30166f8cb0642fc0f0cadba0e6188 --- /dev/null +++ b/spec/presenters/aspect_presenter_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe AspectPresenter do + before do + @presenter = AspectPresenter.new(bob.aspects.first) + end + + describe '#to_json' do + it 'works' do + @presenter.to_json.should be_present + end + end +end \ No newline at end of file diff --git a/spec/presenters/service_presenter_spec.rb b/spec/presenters/service_presenter_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..fe3c8a418f95b77fdeb2569530f6b62741060830 --- /dev/null +++ b/spec/presenters/service_presenter_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' + +describe ServicePresenter do + describe '#as_json' do + it 'includes the provider name of the json' do + presenter = ServicePresenter.new(stub(:provider => "fakebook")) + presenter.as_json[:provider].should == 'fakebook' + end + end +end \ No newline at end of file diff --git a/spec/presenters/user_presenter_spec.rb b/spec/presenters/user_presenter_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..9406741c95b31907a91b242b41ffd8880ff66522 --- /dev/null +++ b/spec/presenters/user_presenter_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe UserPresenter do + before do + @presenter = UserPresenter.new(bob) + end + + describe '#to_json' do + it 'works' do + @presenter.to_json.should be_present + end + end + + describe '#aspects' do + it 'provides an array of the jsonified aspects' do + aspect = bob.aspects.first + @presenter.aspects.first[:id].should == aspect.id + @presenter.aspects.first[:name].should == aspect.name + end + end + + describe '#services' do + it 'provides an array of jsonifed services' do + fakebook = stub(:provider => 'fakebook') + bob.stub(:services).and_return([fakebook]) + @presenter.services.should include(:provider => 'fakebook') + end + end +end \ No newline at end of file