diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index d736be793..e45a792f3 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -58,6 +58,7 @@ //= require legislation_allegations //= require legislation_annotatable //= require watch_form_changes +//= require followable //= require tree_navigator //= require custom @@ -93,6 +94,7 @@ var initialize_modules = function() { App.LegislationAnnotatable.initialize(); App.WatchFormChanges.initialize(); App.TreeNavigator.initialize(); + App.Followable.initialize(); }; $(function(){ diff --git a/app/assets/javascripts/followable.js.coffee b/app/assets/javascripts/followable.js.coffee new file mode 100644 index 000000000..69ea61bac --- /dev/null +++ b/app/assets/javascripts/followable.js.coffee @@ -0,0 +1,10 @@ +App.Followable = + + initialize: -> + $('.followable-content a[data-toggle]').on 'click', (event) -> + event.preventDefault() + + update: (followable_id, button) -> + $("#" + followable_id + " .js-follow").html(button) + # Temporary line. Waiting for issue resolution: https://github.com/consul/consul/issues/1736 + initialize_modules() diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index e223ffa56..b4ea0cca3 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -1982,6 +1982,14 @@ table { } } +.public-interests { + margin-top: $line-height; + + .column { + padding-left: 0; + } +} + // 18. Banners // ----------- diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb index 3b6808de9..4b64fdc2e 100644 --- a/app/controllers/account_controller.rb +++ b/app/controllers/account_controller.rb @@ -26,7 +26,7 @@ class AccountController < ApplicationController params.require(:account).permit(:phone_number, :email_on_comment, :email_on_comment_reply, :newsletter, organization_attributes: [:name, :responsible_name]) else - params.require(:account).permit(:username, :public_activity, :email_on_comment, :email_on_comment_reply, + params.require(:account).permit(:username, :public_activity, :public_interests, :email_on_comment, :email_on_comment_reply, :email_on_direct_message, :email_digest, :newsletter, :official_position_badge) end end diff --git a/app/controllers/follows_controller.rb b/app/controllers/follows_controller.rb new file mode 100644 index 000000000..9cec999fa --- /dev/null +++ b/app/controllers/follows_controller.rb @@ -0,0 +1,23 @@ +class FollowsController < ApplicationController + before_action :authenticate_user! + load_and_authorize_resource + + def create + @followable = find_followable + @follow = Follow.create(user: current_user, followable: @followable) + render :refresh_follow_button + end + + def destroy + @follow = Follow.find(params[:id]) + @followable = @follow.followable + @follow.destroy + render :refresh_follow_button + end + + private + + def find_followable + params[:followable_type].constantize.find(params[:followable_id]) + end +end diff --git a/app/controllers/proposal_notifications_controller.rb b/app/controllers/proposal_notifications_controller.rb index 36e265e38..dfad56945 100644 --- a/app/controllers/proposal_notifications_controller.rb +++ b/app/controllers/proposal_notifications_controller.rb @@ -11,8 +11,8 @@ class ProposalNotificationsController < ApplicationController @notification = ProposalNotification.new(proposal_notification_params) @proposal = Proposal.find(proposal_notification_params[:proposal_id]) if @notification.save - @proposal.voters.each do |voter| - Notification.add(voter.id, @notification) + @proposal.users_to_notify.each do |user| + Notification.add(user.id, @notification) end redirect_to @notification, notice: I18n.t("flash.actions.create.proposal_notification") else @@ -30,4 +30,4 @@ class ProposalNotificationsController < ApplicationController params.require(:proposal_notification).permit(:title, :body, :proposal_id) end -end \ No newline at end of file +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index e99102396..6ef59fb60 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -7,6 +7,7 @@ class UsersController < ApplicationController def show load_filtered_activity if valid_access? + load_interests if valid_interests_access? end private @@ -62,10 +63,18 @@ class UsersController < ApplicationController @budget_investments = Budget::Investment.where(author_id: @user.id).order(created_at: :desc).page(params[:page]) end + def load_interests + @user.interests + end + def valid_access? @user.public_activity || authorized_current_user? end + def valid_interests_access? + @user.public_interests || authorized_current_user? + end + def author? @author ||= current_user && (current_user == @user) end diff --git a/app/helpers/flags_helper.rb b/app/helpers/flags_helper.rb index 715937c0f..b5ba67f41 100644 --- a/app/helpers/flags_helper.rb +++ b/app/helpers/flags_helper.rb @@ -26,4 +26,4 @@ module FlagsHelper end end -end \ No newline at end of file +end diff --git a/app/helpers/follows_helper.rb b/app/helpers/follows_helper.rb new file mode 100644 index 000000000..63f50f7be --- /dev/null +++ b/app/helpers/follows_helper.rb @@ -0,0 +1,61 @@ +module FollowsHelper + + def show_follow_action?(followable) + current_user && !followed?(followable) + end + + def show_unfollow_action?(followable) + current_user && followed?(followable) + end + + def follow_entity_text(followable) + entity = followable.class.name.gsub('::', '/').downcase + t('shared.follow_entity', entity: t("activerecord.models.#{entity}.one").downcase) + end + + def follow_entity_title(followable) + entity = followable.class.name.gsub('::', '/').downcase + t('shared.follow_entity_title', entity: t("activerecord.models.#{entity}.one").downcase) + end + + def unfollow_entity_text(followable) + entity = followable.class.name.gsub('::', '/').downcase + t('shared.unfollow_entity', entity: t("activerecord.models.#{entity}.one").downcase) + end + + def entity_full_name(followable) + name = followable.class.name + name.downcase.gsub("::", "-") + end + + def follow_link_wrapper_id(followable) + "follow-expand-#{entity_full_name(followable)}-#{followable.id}" + end + + def unfollow_link_wrapper_id(followable) + "unfollow-expand-#{entity_full_name(followable)}-#{followable.id}" + end + + def follow_link_id(followable) + "follow-#{entity_full_name(followable)}-#{followable.id}" + end + + def unfollow_link_id(followable) + "unfollow-#{entity_full_name(followable)}-#{followable.id}" + end + + def follow_drop_id(followable) + "follow-drop-#{entity_full_name(followable)}-#{followable.id}" + end + + def unfollow_drop_id(followable) + "unfollow-drop-#{entity_full_name(followable)}-#{followable.id}" + end + + private + + def followed?(followable) + Follow.followed?(current_user, followable) + end + +end diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb index 2b32abd07..cf183ec53 100644 --- a/app/models/abilities/common.rb +++ b/app/models/abilities/common.rb @@ -34,6 +34,8 @@ module Abilities can [:flag, :unflag], Proposal cannot [:flag, :unflag], Proposal, author_id: user.id + can [:create, :destroy], Follow + unless user.organization? can :vote, Debate can :vote, Comment diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index 6a83b28a2..a5bea4508 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -6,6 +6,7 @@ class Budget include Taggable include Searchable include Reclassification + include Followable acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/concerns/followable.rb b/app/models/concerns/followable.rb new file mode 100644 index 000000000..51469698e --- /dev/null +++ b/app/models/concerns/followable.rb @@ -0,0 +1,9 @@ +module Followable + extend ActiveSupport::Concern + + included do + has_many :follows, as: :followable, dependent: :destroy + has_many :followers, through: :follows, source: :user + end + +end diff --git a/app/models/follow.rb b/app/models/follow.rb new file mode 100644 index 000000000..6c2c62e99 --- /dev/null +++ b/app/models/follow.rb @@ -0,0 +1,20 @@ +class Follow < ActiveRecord::Base + belongs_to :user + belongs_to :followable, polymorphic: true + + validates :user_id, presence: true + validates :followable_id, presence: true + validates :followable_type, presence: true + + scope(:by_user_and_followable, lambda do |user, followable| + where(user_id: user.id, + followable_type: followable.class.to_s, + followable_id: followable.id) + end) + + def self.followed?(user, followable) + return false unless user + !! by_user_and_followable(user, followable).try(:first) + end + +end diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 9e02c197e..21335c5a5 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -8,6 +8,7 @@ class Proposal < ActiveRecord::Base include Filterable include HasPublicAuthor include Graphqlable + include Followable acts_as_votable acts_as_paranoid column: :hidden_at @@ -170,6 +171,10 @@ class Proposal < ActiveRecord::Base proposal_notifications end + def users_to_notify + (voters + followers).uniq + end + protected def set_responsible_name diff --git a/app/models/user.rb b/app/models/user.rb index b41057157..60536195b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -31,6 +31,7 @@ class User < ActiveRecord::Base has_many :direct_messages_sent, class_name: 'DirectMessage', foreign_key: :sender_id has_many :direct_messages_received, class_name: 'DirectMessage', foreign_key: :receiver_id has_many :legislation_answers, class_name: 'Legislation::Answer', dependent: :destroy, inverse_of: :user + has_many :follows belongs_to :geozone validates :username, presence: true, if: :username_required? @@ -308,6 +309,10 @@ class User < ActiveRecord::Base where(conditions.to_hash).where(["username = ?", login]).first end + def interests + follows.map{|follow| follow.followable.tags.map(&:name)}.flatten.compact.uniq + end + private def clean_document_number diff --git a/app/views/account/show.html.erb b/app/views/account/show.html.erb index a0d9f1693..a3407f5a7 100644 --- a/app/views/account/show.html.erb +++ b/app/views/account/show.html.erb @@ -40,6 +40,16 @@ <% end %> +
+ <%= f.label :public_interests do %> + <%= f.check_box :public_interests, title: t('account.show.public_interests_label'), label: false %> + + <%= t("account.show.public_interests_label") %> + + <% end %> +
+ + <% if @account.email.present? %>

<%= t("account.show.notifications")%>

diff --git a/app/views/budgets/investments/_investment_show.html.erb b/app/views/budgets/investments/_investment_show.html.erb index 42a8b9f26..950b93b6a 100644 --- a/app/views/budgets/investments/_investment_show.html.erb +++ b/app/views/budgets/investments/_investment_show.html.erb @@ -112,6 +112,9 @@ title: investment.title, url: budget_investment_url(budget_id: investment.budget_id, id: investment.id) } %> + + <%= render 'follows/followable_button', followable: investment if current_user %> + diff --git a/app/views/follows/_followable_button.html.erb b/app/views/follows/_followable_button.html.erb new file mode 100644 index 000000000..527a7e029 --- /dev/null +++ b/app/views/follows/_followable_button.html.erb @@ -0,0 +1,45 @@ + + + + <% if show_follow_action? followable %> + + <%= link_to "##{follow_link_wrapper_id(followable)}", + id: follow_link_wrapper_id(followable), + title: follow_entity_text(followable), + data: { toggle: follow_drop_id(followable) }, + class: 'button hollow' do %> + <%= t('shared.follow') %> + <% end %> + + + <% end %> + + <% if show_unfollow_action? followable %> + + <% follow = followable.follows.where(user: current_user).first %> + <%= link_to "##{unfollow_link_wrapper_id(followable)}", + id: unfollow_link_wrapper_id(followable), + title: unfollow_entity_text(followable), + data: { toggle: unfollow_drop_id(followable) }, + class: 'button hollow' do %> + <%= t('shared.unfollow') %> + <% end %> + + + <% end %> + + + \ No newline at end of file diff --git a/app/views/follows/refresh_follow_button.js.erb b/app/views/follows/refresh_follow_button.js.erb new file mode 100644 index 000000000..31e2627a3 --- /dev/null +++ b/app/views/follows/refresh_follow_button.js.erb @@ -0,0 +1,2 @@ +App.Followable.update("<%= dom_id(@followable) %>", + "<%= j render('followable_button', followable: @followable) %>") diff --git a/app/views/proposal_notifications/new.html.erb b/app/views/proposal_notifications/new.html.erb index 678e00088..55341eb58 100644 --- a/app/views/proposal_notifications/new.html.erb +++ b/app/views/proposal_notifications/new.html.erb @@ -7,7 +7,7 @@

<%= t("proposal_notifications.new.info_about_receivers_html", - count: @proposal.voters.count, + count: @proposal.users_to_notify.count, proposal_page: link_to(t("proposal_notifications.new.proposal_page"), proposal_path(@proposal, anchor: "comments"))).html_safe %>

diff --git a/app/views/proposals/_flag_actions.html.erb b/app/views/proposals/_flag_actions.html.erb index aba7a2423..875a23f07 100644 --- a/app/views/proposals/_flag_actions.html.erb +++ b/app/views/proposals/_flag_actions.html.erb @@ -1,19 +1,21 @@ - - <% if show_flag_action? proposal %> - - - - - <% end %> + + + <% if show_flag_action? proposal %> + + + + + <% end %> - <% if show_unflag_action? proposal %> - - - - - <% end %> + <% if show_unflag_action? proposal %> + + + + + <% end %> + diff --git a/app/views/proposals/show.html.erb b/app/views/proposals/show.html.erb index d678214d2..d5c83d9f5 100644 --- a/app/views/proposals/show.html.erb +++ b/app/views/proposals/show.html.erb @@ -49,10 +49,12 @@  •    <%= link_to t("proposals.show.comments", count: @proposal.comments_count), "#comments" %> -  •  - + + <% if current_user %> +  •  <%= render 'proposals/flag_actions', proposal: @proposal %> - + <% end %> +

@@ -137,6 +139,9 @@ title: @proposal.title, url: proposal_url(@proposal) } %> + + <%= render 'follows/followable_button', followable: @proposal if current_user %> + diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index c00769216..fb4a3d1f9 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -46,6 +46,21 @@

<%= t('users.show.private_activity') %>

<% end %> + <% if @user.public_interests || @authorized_current_user %> +
+

<%= t('account.show.public_interests_title_list') %>

+ <% @user.interests.in_groups_of(10, false) do |interests_group| %> +
+ +
+ <% end %> +
+ <% end %> + diff --git a/config/locales/en/general.yml b/config/locales/en/general.yml index 8c0763b32..fd5a128b8 100644 --- a/config/locales/en/general.yml +++ b/config/locales/en/general.yml @@ -13,6 +13,8 @@ en: personal: Personal details phone_number_label: Phone number public_activity_label: Keep my list of activities public + public_interests_label: Keep my interests public + public_interests_title_list: List of interests save_changes_submit: Save changes subscription_to_website_newsletter_label: Receive by email website relevant information email_on_direct_message_label: Receive emails about direct messages @@ -500,6 +502,9 @@ en: check_none: None collective: Collective flag: Flag as inappropriate + follow: "Follow" + follow_entity: "Follow %{entity}" + follow_entity_title: "Follow %{entity}: You can participate and receive notifications of any related events." hide: Hide print: print_button: Print this info @@ -532,6 +537,8 @@ en: target_blank_html: " (link opens in new window)" you_are_in: "You are in" unflag: Unflag + unfollow: "Unfollow" + unfollow_entity: "Unfollow %{entity}" outline: debates: Debates proposals: Proposals @@ -643,6 +650,9 @@ en: budget_investments: one: 1 Investment other: "%{count} Investments" + follows: + one: 1 Following + other: "%{count} Following" no_activity: User has no public activity no_private_messages: "This user doesn't accept private messages." private_activity: This user decided to keep the activity list private @@ -708,4 +718,3 @@ en: invisible_captcha: sentence_for_humans: "If you are human, ignore this field" timestamp_error_message: "Sorry, that was too quick! Please resubmit." - diff --git a/config/locales/es/general.yml b/config/locales/es/general.yml index d38184647..941a03813 100644 --- a/config/locales/es/general.yml +++ b/config/locales/es/general.yml @@ -13,6 +13,8 @@ es: personal: Datos personales phone_number_label: Teléfono public_activity_label: Mostrar públicamente mi lista de actividades + public_interests_label: Mostrar públicamente mis intereses + public_interests_title_list: Lista de intereses save_changes_submit: Guardar cambios subscription_to_website_newsletter_label: Recibir emails con información interesante sobre la web email_on_direct_message_label: Recibir emails con mensajes privados @@ -500,6 +502,9 @@ es: check_none: Ninguno collective: Colectivo flag: Denunciar como inapropiado + follow: "Seguir" + follow_entity: "Seguir %{entity}" + follow_entity_title: "Seguir %{entity}: Podrás participar y recibir notificaciones de cualquier suceso relacionado." hide: Ocultar print: print_button: Imprimir esta información @@ -532,6 +537,8 @@ es: target_blank_html: " (se abre en ventana nueva)" you_are_in: "Estás en" unflag: Deshacer denuncia + unfollow: Dejar de seguir + unfollow_entity: "Dejar de seguir %{entity}" outline: debates: Debates proposals: Propuestas @@ -643,6 +650,9 @@ es: budget_investments: one: 1 Proyecto de presupuestos participativos other: "%{count} Proyectos de presupuestos participativos" + follows: + one: 1 Siguiendo + other: "%{count} Siguiendo" no_activity: Usuario sin actividad pública no_private_messages: "Este usuario no acepta mensajes privados." private_activity: Este usuario ha decidido mantener en privado su lista de actividades @@ -707,4 +717,4 @@ es: user_permission_votes: Participar en las votaciones finales* invisible_captcha: sentence_for_humans: "Si eres humano, por favor ignora este campo" - timestamp_error_message: "Eso ha sido demasiado rápido. Por favor, reenvía el formulario." \ No newline at end of file + timestamp_error_message: "Eso ha sido demasiado rápido. Por favor, reenvía el formulario." diff --git a/config/routes.rb b/config/routes.rb index ea12cf1ab..aaa9b5db7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -93,6 +93,8 @@ Rails.application.routes.draw do end end + resources :follows, only: [:create, :destroy] + resources :stats, only: [:index] resources :legacy_legislations, only: [:show], path: 'legislations' diff --git a/db/migrate/20170626180127_create_follows.rb b/db/migrate/20170626180127_create_follows.rb new file mode 100644 index 000000000..d4225cab8 --- /dev/null +++ b/db/migrate/20170626180127_create_follows.rb @@ -0,0 +1,12 @@ +class CreateFollows < ActiveRecord::Migration + def change + create_table :follows do |t| + t.references :user, index: true, foreign_key: true + t.references :followable, polymorphic: true, index: true + + t.timestamps null: false + end + + add_index :follows, [:user_id, :followable_type, :followable_id], name: "access_follows" + end +end diff --git a/db/migrate/20170630105250_add_public_interests_to_user.rb b/db/migrate/20170630105250_add_public_interests_to_user.rb new file mode 100644 index 000000000..8fec4a748 --- /dev/null +++ b/db/migrate/20170630105250_add_public_interests_to_user.rb @@ -0,0 +1,5 @@ +class AddPublicInterestsToUser < ActiveRecord::Migration + def change + add_column :users, :public_interests, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 440fb2a9a..f4c717bd8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -95,9 +95,8 @@ ActiveRecord::Schema.define(version: 20170708225159) do create_table "budget_ballots", force: :cascade do |t| t.integer "user_id" t.integer "budget_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "ballot_lines_count", default: 0 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "budget_groups", force: :cascade do |t| @@ -326,6 +325,18 @@ ActiveRecord::Schema.define(version: 20170708225159) do add_index "flags", ["user_id", "flaggable_type", "flaggable_id"], name: "access_inappropiate_flags", using: :btree add_index "flags", ["user_id"], name: "index_flags_on_user_id", using: :btree + create_table "follows", force: :cascade do |t| + t.integer "user_id" + t.integer "followable_id" + t.string "followable_type" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "follows", ["followable_type", "followable_id"], name: "index_follows_on_followable_type_and_followable_id", using: :btree + add_index "follows", ["user_id", "followable_type", "followable_id"], name: "access_follows", using: :btree + add_index "follows", ["user_id"], name: "index_follows_on_user_id", using: :btree + create_table "geozones", force: :cascade do |t| t.string "name" t.string "html_map_coordinates" @@ -917,6 +928,7 @@ ActiveRecord::Schema.define(version: 20170708225159) do t.boolean "created_from_signature", default: false t.integer "failed_email_digests_count", default: 0 t.text "former_users_data_log", default: "" + t.boolean "public_interests", default: false end add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree @@ -1010,6 +1022,7 @@ ActiveRecord::Schema.define(version: 20170708225159) do add_foreign_key "failed_census_calls", "poll_officers" add_foreign_key "failed_census_calls", "users" add_foreign_key "flags", "users" + add_foreign_key "follows", "users" add_foreign_key "geozones_polls", "geozones" add_foreign_key "geozones_polls", "polls" add_foreign_key "identities", "users" diff --git a/spec/factories.rb b/spec/factories.rb index 9a3c292b3..d9521847d 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -166,8 +166,8 @@ FactoryGirl.define do end trait :flagged do - after :create do |debate| - Flag.flag(FactoryGirl.create(:user), debate) + after :create do |proposal| + Flag.flag(FactoryGirl.create(:user), proposal) end end @@ -349,6 +349,18 @@ FactoryGirl.define do association :user, factory: :user end + factory :follow do + association :user, factory: :user + + trait :followed_proposal do + association :followable, factory: :proposal + end + + trait :followed_investment do + association :followable, factory: :budget_investment + end + end + factory :comment do association :commentable, factory: :debate user diff --git a/spec/features/budgets/investments_spec.rb b/spec/features/budgets/investments_spec.rb index ca86587f0..96a26ad82 100644 --- a/spec/features/budgets/investments_spec.rb +++ b/spec/features/budgets/investments_spec.rb @@ -329,6 +329,14 @@ feature 'Budget Investments' do end end + scenario "Don't display flaggable buttons" do + investment = create(:budget_investment, heading: heading) + + visit budget_investment_path(budget_id: budget.id, id: investment.id) + + expect(page).not_to have_selector ".js-follow" + end + scenario "Show back link contains heading id" do investment = create(:budget_investment, heading: heading) visit budget_investment_path(budget, investment) @@ -421,6 +429,8 @@ feature 'Budget Investments' do end end + it_behaves_like "followable", "budget_investment", "budget_investment_path", {"budget_id": "budget_id", "id": "id"} + context "Destroy" do scenario "Admin cannot destroy budget investments" do diff --git a/spec/features/notifications_spec.rb b/spec/features/notifications_spec.rb index b5a78c38c..a53024109 100644 --- a/spec/features/notifications_spec.rb +++ b/spec/features/notifications_spec.rb @@ -236,6 +236,60 @@ feature "Notifications" do expect(page).to have_css ".notification", count: 0 end + scenario "Followers should receive a notification", :js do + author = create(:user) + + user1 = create(:user) + user2 = create(:user) + user3 = create(:user) + + proposal = create(:proposal, author: author) + + create(:follow, :followed_proposal, user: user1, followable: proposal) + create(:follow, :followed_proposal, user: user2, followable: proposal) + + login_as(author) + visit root_path + + visit new_proposal_notification_path(proposal_id: proposal.id) + + fill_in 'proposal_notification_title', with: "Thank you for supporting my proposal" + fill_in 'proposal_notification_body', with: "Please share it with others so we can make it happen!" + click_button "Send message" + + expect(page).to have_content "Your message has been sent correctly." + + logout + login_as user1 + visit root_path + + find(".icon-notification").click + + notification_for_user1 = Notification.where(user: user1).first + expect(page).to have_css ".notification", count: 1 + expect(page).to have_content "There is one new notification on #{proposal.title}" + expect(page).to have_xpath "//a[@href='#{notification_path(notification_for_user1)}']" + + logout + login_as user2 + visit root_path + + find(".icon-notification").click + + notification_for_user2 = Notification.where(user: user2).first + expect(page).to have_css ".notification", count: 1 + expect(page).to have_content "There is one new notification on #{proposal.title}" + expect(page).to have_xpath "//a[@href='#{notification_path(notification_for_user2)}']" + + logout + login_as user3 + visit root_path + + find(".icon-no-notification").click + + expect(page).to have_css ".notification", count: 0 + end + pending "group notifications for the same proposal" end diff --git a/spec/features/proposal_notifications_spec.rb b/spec/features/proposal_notifications_spec.rb index 2c12c85c8..4756a1903 100644 --- a/spec/features/proposal_notifications_spec.rb +++ b/spec/features/proposal_notifications_spec.rb @@ -36,6 +36,33 @@ feature 'Proposal Notifications' do expect(Notification.count).to eq(1) end + scenario "Send a notification (Follower)" do + author = create(:user) + proposal = create(:proposal, author: author) + user_follower = create(:user) + create(:follow, :followed_proposal, user: user_follower, followable: proposal) + + create_proposal_notification(proposal) + + expect(Notification.count).to eq(1) + end + + scenario "Send a notification (Follower and Voter)" do + author = create(:user) + proposal = create(:proposal, author: author) + + user_voter_follower = create(:user) + create(:follow, :followed_proposal, user: user_voter_follower, followable: proposal) + create(:vote, voter: user_voter_follower, votable: proposal) + + user_follower = create(:user) + create(:follow, :followed_proposal, user: user_follower, followable: proposal) + + create_proposal_notification(proposal) + + expect(Notification.count).to eq(2) + end + scenario "Send a notification (Blocked voter)" do author = create(:user) proposal = create(:proposal, author: author) @@ -77,7 +104,7 @@ feature 'Proposal Notifications' do expect(page).to have_content "We are almost there please share with your peoples!" end - scenario "Message about receivers" do + scenario "Message about receivers (Voters)" do author = create(:user) proposal = create(:proposal, author: author) @@ -90,6 +117,48 @@ feature 'Proposal Notifications' do expect(page).to have_link("the proposal's page", href: proposal_path(proposal, anchor: 'comments')) end + scenario "Message about receivers (Followers)" do + author = create(:user) + proposal = create(:proposal, author: author) + + 7.times { create(:follow, :followed_proposal, followable: proposal) } + + login_as(author) + visit new_proposal_notification_path(proposal_id: proposal.id) + + expect(page).to have_content "This message will be send to 7 people and it will be visible in the proposal's page" + expect(page).to have_link("the proposal's page", href: proposal_path(proposal, anchor: 'comments')) + end + + scenario "Message about receivers (Disctinct Followers and Voters)" do + author = create(:user) + proposal = create(:proposal, author: author) + + 7.times { create(:follow, :followed_proposal, followable: proposal) } + 7.times { create(:vote, votable: proposal, vote_flag: true) } + + login_as(author) + visit new_proposal_notification_path(proposal_id: proposal.id) + + expect(page).to have_content "This message will be send to 14 people and it will be visible in the proposal's page" + expect(page).to have_link("the proposal's page", href: proposal_path(proposal, anchor: 'comments')) + end + + scenario "Message about receivers (Same Followers and Voters)" do + author = create(:user) + proposal = create(:proposal, author: author) + + user_voter_follower = create(:user) + create(:follow, :followed_proposal, user: user_voter_follower, followable: proposal) + create(:vote, voter: user_voter_follower, votable: proposal) + + login_as(author) + visit new_proposal_notification_path(proposal_id: proposal.id) + + expect(page).to have_content "This message will be send to 1 people and it will be visible in the proposal's page" + expect(page).to have_link("the proposal's page", href: proposal_path(proposal, anchor: 'comments')) + end + context "Permissions" do scenario "Link to send the message" do diff --git a/spec/features/proposals_spec.rb b/spec/features/proposals_spec.rb index 5ec0d36fd..1cb39a579 100644 --- a/spec/features/proposals_spec.rb +++ b/spec/features/proposals_spec.rb @@ -61,6 +61,8 @@ feature 'Proposals' do expect(page).to have_content I18n.l(proposal.created_at.to_date) expect(page).to have_selector(avatar(proposal.author.name)) expect(page.html).to include "#{proposal.title}" + expect(page).not_to have_selector ".js-flag-actions" + expect(page).not_to have_selector ".js-follow" within('.social-share-button') do expect(page.all('a').count).to be(4) # Twitter, Facebook, Google+, Telegram @@ -1224,6 +1226,8 @@ feature 'Proposals' do expect(Flag.flagged?(user, proposal)).to_not be end + it_behaves_like "followable", "proposal", "proposal_path", { "id": "id" } + scenario 'Erased author' do user = create(:user) proposal = create(:proposal, author: user) diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index 90ee6b94c..a6e9e2941 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -213,6 +213,105 @@ feature 'Users' do end + feature 'Public interest' do + background do + @user = create(:user) + end + + scenario 'Display interests' do + proposal = create(:proposal, tag_list: "Sport") + create(:follow, :followed_proposal, followable: proposal, user: @user) + + login_as(@user) + visit account_path + + check 'account_public_interests' + click_button 'Save changes' + + logout + + visit user_path(@user) + expect(page).to have_content("Sport") + end + + scenario 'Not display interests when proposal has been destroyed' do + proposal = create(:proposal, tag_list: "Sport") + create(:follow, :followed_proposal, followable: proposal, user: @user) + proposal.destroy + + login_as(@user) + visit account_path + + check 'account_public_interests' + click_button 'Save changes' + + logout + + visit user_path(@user) + expect(page).not_to have_content("Sport") + end + + scenario 'No visible by default' do + visit user_path(@user) + + expect(page).to have_content(@user.username) + expect(page).not_to have_css('#public_interests') + end + + scenario 'User can display public page' do + login_as(@user) + visit account_path + + check 'account_public_interests' + click_button 'Save changes' + + logout + + visit user_path(@user) + expect(page).to have_css('#public_interests') + end + + scenario 'Is always visible for the owner' do + login_as(@user) + visit account_path + + uncheck 'account_public_interests' + click_button 'Save changes' + + visit user_path(@user) + expect(page).to have_css('#public_interests') + end + + scenario 'Is always visible for admins' do + login_as(@user) + visit account_path + + uncheck 'account_public_interests' + click_button 'Save changes' + + logout + + login_as(create(:administrator).user) + visit user_path(@user) + expect(page).to have_css('#public_interests') + end + + scenario 'Is always visible for moderators' do + login_as(@user) + visit account_path + + uncheck 'account_public_interests' + click_button 'Save changes' + + logout + + login_as(create(:moderator).user) + visit user_path(@user) + expect(page).to have_css('#public_interests') + end + + end + feature 'Special comments' do scenario 'comments posted as moderator are not visible in user activity' do diff --git a/spec/models/follow_spec.rb b/spec/models/follow_spec.rb new file mode 100644 index 000000000..fc17924f5 --- /dev/null +++ b/spec/models/follow_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +describe Follow do + + let(:follow) { build(:follow, :followed_proposal) } + + it "should be valid" do + expect(follow).to be_valid + end + + it "should not be valid without a user_id" do + follow.user_id = nil + expect(follow).to_not be_valid + end + + it "should not be valid without a followable_id" do + follow.followable_id = nil + expect(follow).to_not be_valid + end + + it "should not be valid without a followable_type" do + follow.followable_type = nil + expect(follow).to_not be_valid + end + +end diff --git a/spec/models/proposal_spec.rb b/spec/models/proposal_spec.rb index d8c467974..5bf6d90e1 100644 --- a/spec/models/proposal_spec.rb +++ b/spec/models/proposal_spec.rb @@ -857,4 +857,26 @@ describe Proposal do end end + describe "#user_to_notify" do + + it "should return voters and followers" do + proposal = create(:proposal) + voter = create(:user, :level_two) + follower = create(:user, :level_two) + follow = create(:follow, user: follower, followable: proposal) + create(:vote, voter: voter, votable: proposal) + + expect(proposal.users_to_notify).to eq([voter, follower]) + end + + it "should return voters and followers discarding duplicates" do + proposal = create(:proposal) + voter_and_follower = create(:user, :level_two) + follow = create(:follow, user: voter_and_follower, followable: proposal) + create(:vote, voter: voter_and_follower, votable: proposal) + + expect(proposal.users_to_notify).to eq([voter_and_follower]) + end + + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 2e92e5c96..2ca1dfe05 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -657,4 +657,27 @@ describe User do end end + describe "#interests" do + let(:user) { create(:user) } + + it "should return followed object tags" do + proposal = create(:proposal, tag_list: "Sport") + create(:follow, followable: proposal, user: user) + + expect(user.interests).to eq ["Sport"] + end + + it "should discard followed objects duplicated tags" do + proposal1 = create(:proposal, tag_list: "Sport") + proposal2 = create(:proposal, tag_list: "Sport") + budget_investment = create(:budget_investment, tag_list: "Sport") + + create(:follow, followable: proposal1, user: user) + create(:follow, followable: proposal2, user: user) + create(:follow, followable: budget_investment, user: user) + + expect(user.interests).to eq ["Sport"] + end + + end end diff --git a/spec/shared/features/followable.rb b/spec/shared/features/followable.rb new file mode 100644 index 000000000..7285ee15f --- /dev/null +++ b/spec/shared/features/followable.rb @@ -0,0 +1,83 @@ +shared_examples "followable" do |followable_class_name, followable_path, followable_path_arguments| + include ActionView::Helpers + + let!(:arguments) { {} } + let!(:followable) { create(followable_class_name) } + let!(:followable_dom_name) { followable_class_name.gsub('_', '-') } + + before do + followable_path_arguments.each do |argument_name, path_to_value| + arguments.merge!("#{argument_name}": followable.send(path_to_value)) + end + end + + context "Show" do + + scenario "Should not display follow button when there is no logged user" do + visit send(followable_path, arguments) + + within "##{dom_id(followable)}" do + expect(page).not_to have_link("Follow") + end + end + + scenario "Should display follow button when user is logged in" do + user = create(:user) + login_as(user) + + visit send(followable_path, arguments) + + within "##{dom_id(followable)}" do + expect(page).to have_link("Follow") + end + end + + scenario "Should display follow button when user is logged and is not following" do + user = create(:user) + login_as(user) + + visit send(followable_path, arguments) + + expect(page).to have_link("Follow") + end + + scenario "Should display unfollow button when click on follow button", :js do + user = create(:user) + login_as(user) + + visit send(followable_path, arguments) + within "##{dom_id(followable)}" do + click_link "Follow" + page.find("#follow-#{followable_dom_name}-#{followable.id}").click + + expect(page).to have_css("#unfollow-expand-#{followable_dom_name}-#{followable.id}") + end + end + + scenario "Display unfollow button when user already following" do + user = create(:user) + follow = create(:follow, user: user, followable: followable) + login_as(user) + + visit send(followable_path, arguments) + + expect(page).to have_link("Unfollow") + end + + scenario "Should display follow button when click on unfollow button", :js do + user = create(:user) + follow = create(:follow, user: user, followable: followable) + login_as(user) + + visit send(followable_path, arguments) + within "##{dom_id(followable)}" do + click_link "Unfollow" + page.find("#unfollow-#{followable_dom_name}-#{followable.id}").click + + expect(page).to have_css("#follow-expand-#{followable_dom_name}-#{followable.id}") + end + end + + end + +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7ef336a4d..1548eada8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,6 +5,7 @@ require 'devise' require 'knapsack' Dir["./spec/models/concerns/*.rb"].each { |f| require f } Dir["./spec/support/**/*.rb"].sort.each { |f| require f } +Dir["./spec/shared/**/*.rb"].sort.each { |f| require f } RSpec.configure do |config| config.use_transactional_fixtures = false