diff --git a/app/controllers/aspects_controller.rb b/app/controllers/aspects_controller.rb index ec8af115dc7acf7e21e521f885a7f34e713cefc4..c47257a1657f19a86cfd9cb8ad8b25971365b2e4 100644 --- a/app/controllers/aspects_controller.rb +++ b/app/controllers/aspects_controller.rb @@ -9,6 +9,9 @@ class AspectsController < ApplicationController respond_to :html, :js respond_to :json, :only => [:show, :create] + + helper_method :tags, :tag_followings + helper_method :all_aspects_selected? def index if params[:a_ids] @@ -158,11 +161,23 @@ class AspectsController < ApplicationController params[:max_time] ||= Time.now + 1 end - helper_method :all_aspects_selected? def all_aspects_selected? @aspect == :all end + def tag_followings + if current_user + if @tag_followings == nil + @tag_followings = current_user.tag_followings + end + @tag_followings + end + end + + def tags + @tags ||= current_user.followed_tags + end + private def save_sort_order if params[:sort_order].present? diff --git a/app/controllers/tag_followings_controller.rb b/app/controllers/tag_followings_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..2883f501de9759ae50424113712e2ef1e1f6823a --- /dev/null +++ b/app/controllers/tag_followings_controller.rb @@ -0,0 +1,32 @@ +class TagFollowingsController < ApplicationController + before_filter :authenticate_user! + + # POST /tag_followings + # POST /tag_followings.xml + def create + @tag = ActsAsTaggableOn::Tag.find_or_create_by_name(params[:name]) + @tag_following = current_user.tag_followings.new(:tag_id => @tag.id) + + if @tag_following.save + flash[:notice] = I18n.t('tag_followings.create.success', :name => params[:name]) + else + flash[:error] = I18n.t('tag_followings.create.failure', :name => params[:name]) + end + + redirect_to tag_path(:name => params[:name]) + end + + # DELETE /tag_followings/1 + # DELETE /tag_followings/1.xml + def destroy + @tag = ActsAsTaggableOn::Tag.find_by_name(params[:name]) + @tag_following = current_user.tag_followings.where(:tag_id => @tag.id).first + if @tag_following && @tag_following.destroy + flash[:notice] = I18n.t('tag_followings.destroy.success', :name => params[:name]) + else + flash[:error] = I18n.t('tag_followings.destroy.failure', :name => params[:name]) + end + + redirect_to tag_path(:name => params[:name]) + end +end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 1459148567efb17e8043e6e5f5692883812e5a5b..fe9e18e1f10e48b959298bddcdc35df3421de8fe 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -8,6 +8,8 @@ class TagsController < ApplicationController skip_before_filter :set_grammatical_gender before_filter :ensure_page, :only => :show + helper_method :tag_followed? + respond_to :html, :only => [:show] respond_to :json, :only => [:index] @@ -41,8 +43,10 @@ class TagsController < ApplicationController def show @aspect = :tag if current_user - @posts = StatusMessage.joins(:contacts).where(:pending => false).where( - Contact.arel_table[:user_id].eq(current_user.id).or( + @posts = StatusMessage. + joins("LEFT OUTER JOIN post_visibilities ON post_visibilities.post_id = posts.id"). + joins("LEFT OUTER JOIN contacts ON contacts.id = post_visibilities.contact_id"). + where(Contact.arel_table[:user_id].eq(current_user.id).or( StatusMessage.arel_table[:public].eq(true).or( StatusMessage.arel_table[:author_id].eq(current_user.person.id) ) @@ -55,7 +59,6 @@ class TagsController < ApplicationController max_time = params[:max_time] ? Time.at(params[:max_time].to_i) : Time.now @posts = @posts.where(StatusMessage.arel_table[:created_at].lt(max_time)) - @posts = @posts.includes(:comments, :photos).order('posts.created_at DESC').limit(15) @posts = PostsFake.new(@posts) @@ -69,4 +72,11 @@ class TagsController < ApplicationController @people_count = Person.where(:id => profiles.map{|p| p.person_id}).count end end + + def tag_followed? + if @tag_followed.nil? + @tag_followed = TagFollowing.joins(:tag).where(:tags => {:name => params[:name]}, :user_id => current_user.id).exists? #, + end + @tag_followed + end end diff --git a/app/helpers/tag_followings_helper.rb b/app/helpers/tag_followings_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..03bd57800813e883c2e56f5b9557f31c5ecb17a5 --- /dev/null +++ b/app/helpers/tag_followings_helper.rb @@ -0,0 +1,2 @@ +module TagFollowingsHelper +end diff --git a/app/models/tag_following.rb b/app/models/tag_following.rb new file mode 100644 index 0000000000000000000000000000000000000000..6fd07c0eff12b1a907bd0d6a290c61166b24d665 --- /dev/null +++ b/app/models/tag_following.rb @@ -0,0 +1,6 @@ +class TagFollowing < ActiveRecord::Base + belongs_to :user + belongs_to :tag, :class_name => "ActsAsTaggableOn::Tag" + + validates_uniqueness_of :tag_id, :scope => :user_id +end diff --git a/app/models/user.rb b/app/models/user.rb index 0bab1c58ead984b722c606128cdeafe22a2242e3..d93087837189cde29c55f399b7ca3f43b779f6d9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -39,6 +39,8 @@ class User < ActiveRecord::Base has_many :contact_people, :through => :contacts, :source => :person has_many :services, :dependent => :destroy has_many :user_preferences, :dependent => :destroy + has_many :tag_followings, :dependent => :destroy + has_many :followed_tags, :through => :tag_followings, :source => :tag has_many :authorizations, :class_name => 'OAuth2::Provider::Models::ActiveRecord::Authorization', :foreign_key => :resource_owner_id has_many :applications, :through => :authorizations, :source => :client diff --git a/app/views/aspects/index.html.haml b/app/views/aspects/index.html.haml index 26824860dbe499eb7dddcd90d0ce2ff3ec816f55..955be317670b245338858f2b30065e9c54ce2790 100644 --- a/app/views/aspects/index.html.haml +++ b/app/views/aspects/index.html.haml @@ -15,6 +15,17 @@ .section = render 'aspects/aspect_listings' + .section + %ul.left_nav + %li + %div.root_element + = t('aspects.index.tags_following') + + %ul.sub_nav + - for tg in tags + %li + = link_to "##{tg.name}", tag_path(:name => tg.name), :class => "tag_selector" + .span-13.append-1.prepend-5 #aspect_stream_container.stream_container = render 'aspect_stream', diff --git a/app/views/tags/show.haml b/app/views/tags/show.haml index ee681076c145ac0343ce04a6763b5f54b1c8c422..7fe40c9bbd8d0d2dfa96e65e8c9839b73c662b03 100644 --- a/app/views/tags/show.haml +++ b/app/views/tags/show.haml @@ -2,6 +2,7 @@ -# licensed under the Affero General Public License version 3 or later. See -# the COPYRIGHT file. + - content_for :page_title do - if params[:name] = "##{params[:name]}" @@ -11,29 +12,48 @@ - content_for :head do = include_javascripts :home :javascript - $(".people_stream .pagination a").live("click", function() { - $.getScript(this.href); - return false; + $(document).ready(function(){ + $(".button.tag_following").hover(function(){ + $this = $(this); + $this.removeClass("in_aspects"); + $this.val("#{t('.stop_following', :tag => params[:name])}"); + }, + function(){ + $this = $(this); + $this.addClass("in_aspects"); + $this.val("#{t('.following', :tag => params[:name])}"); + }); }); + - content_for :body_class do = "tags_show" -.span-24.last - %h1.tag - = "##{params[:name]}" - -.span-13 - #main_stream.stream - - if @posts.length > 0 - = render 'shared/stream', :posts => @posts - #pagination - =link_to(t('more'), next_page_path, :class => 'paginate') - - else - = t('.nobody_talking', :tag => "##{params[:name]}") - -.prepend-2.span-9.last +.span-6 %h3 = t('people', :count => @people_count) .side_stream.stream = render :partial => 'people/index', :locals => {:people => @people} + +.span-15.last + .stream_container + #author_info + - if user_signed_in? && current_user.person != @person + .right + - unless tag_followed? + = button_to t('.follow', :tag => params[:name]), tag_tag_followings_path(:name => params[:name]), :method => :post, :class => 'button take_action' + - else + = button_to t('.following', :tag => params[:name]), tag_tag_followings_path(:name => params[:name]), :method => :delete, :class => 'button red_on_hover tag_following in_aspects take_action' + %h2 + = "##{params[:name]}" + + %hr + + #main_stream.stream + - if @posts.length > 0 + = render 'shared/stream', :posts => @posts + #pagination + =link_to(t('more'), next_page_path, :class => 'paginate') + - else + = t('.nobody_talking', :tag => "##{params[:name]}") + diff --git a/config/locales/diaspora/en.yml b/config/locales/diaspora/en.yml index dd352603bc0db7761543d10a6d95ea03a096616f..b8a609eb0e3586156964f6d3aaf70e156d86a991 100644 --- a/config/locales/diaspora/en.yml +++ b/config/locales/diaspora/en.yml @@ -146,6 +146,7 @@ en: work: "Work" index: your_aspects: "Your Aspects" + tags_following: "Followed Tags" handle_explanation: "This is your diaspora id. Like an email address, you can give this to people to reach you." no_contacts: "No contacts" post_a_message: "post a message >>" @@ -697,6 +698,17 @@ en: posts_tagged_with: "Posts tagged with #%{tag}" nobody_talking: "Nobody is talking about %{tag} yet." people_tagged_with: "People tagged with %{tag}" + follow: "Follow #%{tag}" + following: "Following #%{tag}" + stop_following: "Stop Following #%{tag}" + + tag_followings: + create: + success: "Successfully following: #%{name}" + failure: "Failed to follow: #%{name}" + destroy: + success: "Successfully stopped following: #%{name}" + failure: "Failed to stop following: #%{name}" tokens: show: diff --git a/config/routes.rb b/config/routes.rb index 7627953bd2c6e59aceb8cee9e2deb9ea90ccfdd1..1dd4938fad5baf4dd462b17690b1266625e843b1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,6 +4,7 @@ Diaspora::Application.routes.draw do + # Posting and Reading resources :aspects do @@ -34,6 +35,9 @@ Diaspora::Application.routes.draw do end resources :tags, :only => [:index] + post "/tags/:name/tag_followings" => "tag_followings#create", :as => 'tag_tag_followings' + delete "/tags/:name/tag_followings" => "tag_followings#destroy" + get 'tags/:name' => 'tags#show', :as => 'tag' resources :apps, :only => [:show] diff --git a/db/migrate/20110701215925_create_tag_followings.rb b/db/migrate/20110701215925_create_tag_followings.rb new file mode 100644 index 0000000000000000000000000000000000000000..c8bb4afbe95b41f9426ad10e4a6aabe2623bb787 --- /dev/null +++ b/db/migrate/20110701215925_create_tag_followings.rb @@ -0,0 +1,14 @@ +class CreateTagFollowings < ActiveRecord::Migration + def self.up + create_table :tag_followings do |t| + t.integer :tag_id + t.integer :user_id + + t.timestamps + end + end + + def self.down + drop_table :tag_followings + end +end diff --git a/db/schema.rb b/db/schema.rb index 085facef9ece0468a2ed1ce00e842f70ab749b92..a702f02810bebe520579b3b81750e3101a528a18 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -326,6 +326,13 @@ ActiveRecord::Schema.define(:version => 20110705003445) do add_index "services", ["user_id"], :name => "index_services_on_user_id" + create_table "tag_followings", :force => true do |t| + t.integer "tag_id" + t.integer "user_id" + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "taggings", :force => true do |t| t.integer "tag_id" t.integer "taggable_id" diff --git a/features/follows_tags.feature b/features/follows_tags.feature new file mode 100644 index 0000000000000000000000000000000000000000..6271ffed2c7359a10b0efe5ed8f5b741e29abfb7 --- /dev/null +++ b/features/follows_tags.feature @@ -0,0 +1,45 @@ +@javascript +Feature: posting + In order to takeover humanity for the good of society + As a rock star + I want to see what humanity is saying about particular tags + + Background: + Given a user with username "bob" + And a user with username "alice" + When I sign in as "bob@bob.bob" + + And I am on the home page + + And I expand the publisher + And I fill in "status_message_fake_text" with "I am da #boss" + And I press the first ".public_icon" within "#publisher" + And I press "Share" + And I wait for the ajax to finish + And I wait for the ajax to finish + + And I follow "#boss" + And I wait for the ajax to finish + Then I should see "I am da #boss" + + + And I go to the destroy user session page + + And I sign in as "alice@alice.alice" + And I search for "#boss" + And I press "Follow #boss" + And I wait for the ajax to finish + + Scenario: see a tag that I am following + When I go to the home page + And I follow "#boss" + Then I should see "I am da #boss" + + Scenario: can stop following a particular tag + When I press "Stop Following #boss" + + And I go to the home page + Then I should not see "#boss" within ".left_nav" + + + diff --git a/features/signs_up.feature b/features/signs_up.feature index 1796cc531806b1d2148be8b2dc87b10ae5d51887..566b6ebfd04112616de1917007e83eb99722db20 100644 --- a/features/signs_up.feature +++ b/features/signs_up.feature @@ -16,7 +16,7 @@ Feature: new user registration And I fill in "profile_last_name" with "Hai" And I fill in "tags" with "#tags" And I press "Save and continue" - And I wait for "step 2" to load + And I wait for the ajax to finish Then I should see "Profile updated" And I should see "Would you like to find your Facebook friends on Diaspora?" And I follow "Skip" @@ -31,5 +31,4 @@ Feature: new user registration Scenario: new user skips the setup wizard When I follow "skip getting started" - And I wait for "the aspects page" to load Then I should be on the aspects page diff --git a/features/step_definitions/custom_web_steps.rb b/features/step_definitions/custom_web_steps.rb index c61534ed3d1a7d557e02f70b15e3576b8f3ede7b..cb8548963d97709d024cd7f99da32a1307d19d81 100644 --- a/features/step_definitions/custom_web_steps.rb +++ b/features/step_definitions/custom_web_steps.rb @@ -129,15 +129,6 @@ When /^I click ok in the confirm dialog to appear next$/ do JS end -When /^I wait for "([^\"]*)" to load$/ do |page_name| - wait_until(10) do - uri = URI.parse(current_url) - current_location = uri.path - current_location << "?#{uri.query}" unless uri.query.blank? - current_location == path_to(page_name) - end -end - Then /^I should get download alert$/ do page.evaluate_script("window.alert = function() { return true; }") end @@ -183,7 +174,7 @@ And /^I scroll down$/ do wait_until(10) { evaluate_script('$("#infscr-loading:visible").length') == 0 } end -When /^I wait for (\d+) seconds$/ do |seconds| +When /^I wait for (\d+) seconds?$/ do |seconds| sleep seconds.to_i end diff --git a/public/stylesheets/sass/application.sass b/public/stylesheets/sass/application.sass index 8077519ca73761c6569e471c1c78fa9b7efb4716..a555a3eafaad67ef5273299e1c55cc245f9f28fc 100644 --- a/public/stylesheets/sass/application.sass +++ b/public/stylesheets/sass/application.sass @@ -2786,6 +2786,7 @@ ul#requested-scopes ul.left_nav :margin 0 + :bottom 20px :padding 0 li @@ -2793,11 +2794,16 @@ ul.left_nav :width 100% a.aspect_selector, - a.home_selector + a.home_selector, + a.tag_selector, + .root_element :display block :width 100% :padding 3px 7px + a.aspect_selector, + a.home_selector, + a.tag_selector &:hover @include border-radius(2px) @@ -2841,8 +2847,10 @@ ul.left_nav :margin 0 li :width 155px + a.aspect_selector, - a.new_aspect + a.new_aspect, + a.tag_selector :padding :left 15px :width 182px @@ -2862,7 +2870,8 @@ ul.left_nav :width 140px a.aspect_selector, - a.new_aspect + a.new_aspect, + a.tag_selector :width 140px li:hover diff --git a/public/stylesheets/sass/ui.sass b/public/stylesheets/sass/ui.sass index 3f27cb274dfc7212336c8caf2f5b17b31f2cd4de..4a34b1ae459fe05758c760938fb09425b8844472 100644 --- a/public/stylesheets/sass/ui.sass +++ b/public/stylesheets/sass/ui.sass @@ -11,6 +11,7 @@ @include border-radius(2px) @include linear-gradient(rgb(248,250,250),rgb(228,223,223)) @include box-shadow(0,1px,1px,#cfcfcf) + @include transition(width, 3s) :font :style normal @@ -82,6 +83,11 @@ &:hover @include linear-gradient(lighten($creation-blue,3%), darken($creation-blue, 8%)) +.button.red_on_hover + &:hover + @include linear-gradient(desaturate(lighten($red, 20%),20%), desaturate(lighten($red,14%),20%)) + :color black + .right :position absolute :right 0 diff --git a/spec/controllers/aspects_controller_spec.rb b/spec/controllers/aspects_controller_spec.rb index 029c7ff081965be37ad80f6c4a4b3f809a57b0c1..8308737383cbacd090ad9a89c4a8cfc197632b9a 100644 --- a/spec/controllers/aspects_controller_spec.rb +++ b/spec/controllers/aspects_controller_spec.rb @@ -331,4 +331,23 @@ describe AspectsController do @alices_aspect_1.reload.contacts_visible.should be_false end end + + context 'helper methods' do + before do + @tag = ActsAsTaggableOn::Tag.create!(:name => "partytimeexcellent") + TagFollowing.create!(:tag => @tag, :user => alice ) + alice.should_receive(:followed_tags).once.and_return([42]) + end + + describe 'tags' do + it 'queries current_users tag if there are tag_followings' do + @controller.tags.should == [42] + end + + it 'does not query twice' do + @controller.tags.should == [42] + @controller.tags.should == [42] + end + end + end end diff --git a/spec/controllers/tag_followings_controller_spec.rb b/spec/controllers/tag_followings_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..60dfb6bdc4aeae471ed9e59bffa7c8754929d448 --- /dev/null +++ b/spec/controllers/tag_followings_controller_spec.rb @@ -0,0 +1,89 @@ +# Copyright (c) 2010, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +require 'spec_helper' + +describe TagFollowingsController do + + def valid_attributes + {:name => "partytimeexcellent"} + end + + before do + @tag = ActsAsTaggableOn::Tag.create!(:name => "partytimeexcellent") + sign_in :user, bob + end + + describe "POST create" do + describe "with valid params" do + it "creates a new TagFollowing" do + expect { + post :create, valid_attributes + }.to change(TagFollowing, :count).by(1) + end + + it "assigns a newly created tag_following as @tag_following" do + post :create, valid_attributes + assigns(:tag_following).should be_a(TagFollowing) + assigns(:tag_following).should be_persisted + end + + it 'creates the tag if it does not already exist' do + expect { + post :create, :name => "tomcruisecontrol" + }.to change(ActsAsTaggableOn::Tag, :count).by(1) + end + + it 'does not create the tag following for non signed in user' do + expect { + post :create, valid_attributes.merge(:user_id => alice.id) + }.to_not change(alice.tag_followings, :count).by(1) + end + + it "redirects and flashes success to the tag page" do + post :create, valid_attributes + + response.should redirect_to(tag_path(:name => valid_attributes[:name])) + flash[:notice].should == "Successfully following: ##{valid_attributes[:name]}" + end + + it "redirects and flashes error if you already have a tag" do + TagFollowing.any_instance.stub(:save).and_return(false) + post :create, valid_attributes + + response.should redirect_to(tag_path(:name => valid_attributes[:name])) + flash[:error].should == "Failed to follow: ##{valid_attributes[:name]}" + end + end + end + + describe "DELETE destroy" do + before do + TagFollowing.create!(:tag => @tag, :user => bob ) + TagFollowing.create!(:tag => @tag, :user => alice ) + end + + it "destroys the requested tag_following" do + expect { + delete :destroy, valid_attributes + }.to change(TagFollowing, :count).by(-1) + end + + it "redirects and flashes error if you already don't follow the tag" do + delete :destroy, valid_attributes + + response.should redirect_to(tag_path(:name => valid_attributes[:name])) + flash[:notice].should == "Successfully stopped following: ##{valid_attributes[:name]}" + end + + it "redirects and flashes error if you already don't follow the tag" do + TagFollowing.any_instance.stub(:destroy).and_return(false) + delete :destroy, valid_attributes + + response.should redirect_to(tag_path(:name => valid_attributes[:name])) + flash[:error].should == "Failed to stop following: ##{valid_attributes[:name]}" + end + end + +end diff --git a/spec/controllers/tags_controller_spec.rb b/spec/controllers/tags_controller_spec.rb index 492d66897c218be437eaa8edb6da15cb57ca0961..34d892e5f2afade260a5debabd218333193e1d1a 100644 --- a/spec/controllers/tags_controller_spec.rb +++ b/spec/controllers/tags_controller_spec.rb @@ -62,6 +62,13 @@ describe TagsController do assigns(:posts).models.should == [other_post] response.status.should == 200 end + + it 'displays a public post that was sent to no one' do + stranger = Factory(:user_with_aspect) + stranger_post = stranger.post(:status_message, :text => "#hello", :public => true, :to => 'all') + get :show, :name => 'hello' + assigns(:posts).models.should == [stranger_post] + end end context "not signed in" do @@ -106,4 +113,24 @@ describe TagsController do end end end -end \ No newline at end of file + + context 'helper methods' do + describe 'tag_followed?' do + before do + sign_in bob + @tag = ActsAsTaggableOn::Tag.create!(:name => "partytimeexcellent") + @controller.stub(:current_user).and_return(bob) + @controller.stub(:params).and_return({:name => "partytimeexcellent"}) + end + + it 'returns true if the following already exists' do + TagFollowing.create!(:tag => @tag, :user => bob ) + @controller.tag_followed?.should be_true + end + + it 'returns false if the following does not already exist' do + @controller.tag_followed?.should be_false + end + end + end +end diff --git a/spec/models/tag_following_spec.rb b/spec/models/tag_following_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..3b1614dfab9c185739d8f55e25a938a4a969186a --- /dev/null +++ b/spec/models/tag_following_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe TagFollowing do + before do + @tag = ActsAsTaggableOn::Tag.create(:name => "partytimeexcellent") + TagFollowing.create!(:tag => @tag, :user => alice) + end + + it 'validates uniqueness of tag_following scoped through user' do + TagFollowing.new(:tag => @tag, :user => alice).valid?.should be_false + end + + it 'allows multiple tag followings for different users' do + TagFollowing.new(:tag => @tag, :user => bob).valid?.should be_true + end +end