From ba0ce4e14b59283977a9d4296c42c1c973135d4a Mon Sep 17 00:00:00 2001 From: Julian Herrero Date: Tue, 6 Oct 2015 12:05:34 +0200 Subject: [PATCH 01/64] =?UTF-8?q?a=C3=B1adir=20track=5Factivity=20para=20m?= =?UTF-8?q?ostrar=20futuras=20notificaciones=20a=20los=20usuarios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/application_controller.rb | 8 ++++++++ app/controllers/comments_controller.rb | 1 + app/models/activity.rb | 2 -- app/models/user.rb | 2 +- db/migrate/20150928075646_create_activities.rb | 9 +++++++++ db/schema.rb | 1 + 6 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20150928075646_create_activities.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 841b32498..b2bd92af1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -108,4 +108,12 @@ class ApplicationController < ActionController::Base store_location_for(:user, request.path) end end + + def track_activity(trackable) + if trackable.is_a? Comment + action = trackable.root? ? "debate_comment" : "comment_reply" + activity = current_user.activities.create! action: action, trackable: trackable + add_notifications_for activity + end + end end diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 35406faf0..e05be448a 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -9,6 +9,7 @@ class CommentsController < ApplicationController def create if @comment.save CommentNotifier.new(comment: @comment).process + track_activity @comment else render :new end diff --git a/app/models/activity.rb b/app/models/activity.rb index 977204669..047ccb7dd 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -1,5 +1,4 @@ class Activity < ActiveRecord::Base - belongs_to :actionable, -> { with_hidden }, polymorphic: true belongs_to :user, -> { with_hidden } @@ -24,5 +23,4 @@ class Activity < ActiveRecord::Base def self.by(user) where(user: user) end - end diff --git a/app/models/user.rb b/app/models/user.rb index dd2bc5cf0..0d07643bd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -199,7 +199,7 @@ class User < ActiveRecord::Base def email_required? !erased? end - + def has_official_email? domain = Setting.value_for 'email_domain_for_officials' !email.blank? && ( (email.end_with? "@#{domain}") || (email.end_with? ".#{domain}") ) diff --git a/db/migrate/20150928075646_create_activities.rb b/db/migrate/20150928075646_create_activities.rb new file mode 100644 index 000000000..89b0edcbc --- /dev/null +++ b/db/migrate/20150928075646_create_activities.rb @@ -0,0 +1,9 @@ +class CreateActivities < ActiveRecord::Migration + def change + create_table :activities do |t| + t.belongs_to :user, index: true, foreign_key: true + t.string :action + t.belongs_to :trackable, polymorphic: true + end + end +end diff --git a/db/schema.rb b/db/schema.rb index ca180b297..16b50e4e9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -396,6 +396,7 @@ ActiveRecord::Schema.define(version: 20151215165824) do add_index "votes", ["votable_id", "votable_type", "vote_scope"], name: "index_votes_on_votable_id_and_votable_type_and_vote_scope", using: :btree add_index "votes", ["voter_id", "voter_type", "vote_scope"], name: "index_votes_on_voter_id_and_voter_type_and_vote_scope", using: :btree + add_foreign_key "activities", "users" add_foreign_key "administrators", "users" add_foreign_key "annotations", "legislations" add_foreign_key "annotations", "users" From d9ba3edc2a54b0a7d546e6f87ecb5221a154ebfd Mon Sep 17 00:00:00 2001 From: Julian Herrero Date: Tue, 6 Oct 2015 12:11:20 +0200 Subject: [PATCH 02/64] mostrar notificaciones a los usuarios cuando alguien comenta en su debate o responde a su comentario --- app/controllers/application_controller.rb | 10 ++ app/controllers/notifications_controller.rb | 9 ++ app/helpers/notifications_helper.rb | 16 +++ app/models/notification.rb | 12 +++ app/models/user.rb | 1 + app/views/devise/menu/_login_items.html.erb | 3 + app/views/notifications/index.html.erb | 11 +++ config/locales/en.yml | 3 + config/locales/es.yml | 3 + config/routes.rb | 3 + .../20150928115005_create_notifications.rb | 9 ++ db/schema.rb | 13 +++ .../notifications_controller_spec.rb | 16 +++ spec/factories.rb | 6 ++ spec/features/notifications_spec.rb | 97 +++++++++++++++++++ spec/helpers/notifications_helper_spec.rb | 48 +++++++++ spec/models/notification_spec.rb | 44 +++++++++ 17 files changed, 304 insertions(+) create mode 100644 app/controllers/notifications_controller.rb create mode 100644 app/helpers/notifications_helper.rb create mode 100644 app/models/notification.rb create mode 100644 app/views/notifications/index.html.erb create mode 100644 db/migrate/20150928115005_create_notifications.rb create mode 100644 spec/controllers/notifications_controller_spec.rb create mode 100644 spec/features/notifications_spec.rb create mode 100644 spec/helpers/notifications_helper_spec.rb create mode 100644 spec/models/notification_spec.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b2bd92af1..312617c4e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -116,4 +116,14 @@ class ApplicationController < ActionController::Base add_notifications_for activity end end + + def add_notifications_for(activity) + case activity.action + when "debate_comment" + author = activity.trackable.debate.author + when "comment_reply" + author = activity.trackable.parent.author + end + author.notifications.create!(activity: activity) unless activity.made_by? author + end end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb new file mode 100644 index 000000000..2fb715287 --- /dev/null +++ b/app/controllers/notifications_controller.rb @@ -0,0 +1,9 @@ +class NotificationsController < ApplicationController + before_action :authenticate_user! + load_and_authorize_resource class: "User" + + def index + @notifications = current_user.notifications.unread.recent.for_render + end + +end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb new file mode 100644 index 000000000..d0e7e71a9 --- /dev/null +++ b/app/helpers/notifications_helper.rb @@ -0,0 +1,16 @@ +module NotificationsHelper + + def notification_text_for(notification) + case notification.activity.action + when "debate_comment" + t("comments.notifications.commented_on_your_debate") + when "comment_reply" + t("comments.notifications.replied_to_your_comment") + end + end + + def notifications_class_for(user) + user.notifications.unread.count > 0 ? "with_notifications" : "without_notifications" + end + +end diff --git a/app/models/notification.rb b/app/models/notification.rb new file mode 100644 index 000000000..aff5c38fa --- /dev/null +++ b/app/models/notification.rb @@ -0,0 +1,12 @@ +class Notification < ActiveRecord::Base + belongs_to :user + belongs_to :activity + + scope :unread, -> { where(read: false) } + scope :recent, -> { order(id: :desc) } + scope :for_render, -> { includes(activity: [:user, :trackable]) } + + def timestamp + activity.trackable.created_at + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 0d07643bd..276598bf2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -22,6 +22,7 @@ class User < ActiveRecord::Base has_many :proposals, -> { with_hidden }, foreign_key: :author_id has_many :comments, -> { with_hidden } has_many :failed_census_calls + has_many :notifications validates :username, presence: true, if: :username_required? validates :username, uniqueness: true, if: :username_required? diff --git a/app/views/devise/menu/_login_items.html.erb b/app/views/devise/menu/_login_items.html.erb index b0998949f..5f6608f2f 100644 --- a/app/views/devise/menu/_login_items.html.erb +++ b/app/views/devise/menu/_login_items.html.erb @@ -9,6 +9,9 @@
  • <%= link_to(t("devise_views.menu.login_items.logout"), destroy_user_session_path, method: :delete) %>
  • +
  • + <%= link_to 'Notificaciones', notifications_path, class: notifications_class_for(current_user) %> +
  • <% else %>
  • <%= link_to(t("devise_views.menu.login_items.login"), new_user_session_path) %> diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb new file mode 100644 index 000000000..222907c7e --- /dev/null +++ b/app/views/notifications/index.html.erb @@ -0,0 +1,11 @@ +
    +
      + <% @notifications.each do |notification| %> +
    • +  •  + <%= notification.activity.username %> <%= notification_text_for(notification) %> + <%= link_to notification.activity.trackable.debate.title, notification.activity.trackable.debate %> +
    • + <% end %> +
    +
    diff --git a/config/locales/en.yml b/config/locales/en.yml index 3ee698c91..a6a4d55bc 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -267,6 +267,9 @@ en: zero: "No votes" one: "1 vote" other: "%{count} votes" + notifications: + commented_on_your_debate: "commented on yout debate" + replied_to_your_comment: "replied to your comment on" comments_helper: comment_link: "Comment" comment_button: "Publish comment" diff --git a/config/locales/es.yml b/config/locales/es.yml index 46bb3b88b..c10d8a493 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -267,6 +267,9 @@ es: zero: "Sin votos" one: "1 voto" other: "%{count} votos" + notifications: + commented_on_your_debate: "ha comentado en tu debate" + replied_to_your_comment: "ha respondido a tu comentario en" comments_helper: comment_link: "Comentar" comment_button: "Publicar comentario" diff --git a/config/routes.rb b/config/routes.rb index 3472f1c4e..681eff89b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,6 +74,9 @@ Rails.application.routes.draw do resource :account, controller: "account", only: [:show, :update, :delete] do collection { get :erase } end + + resources :notifications, only: :index + resource :verification, controller: "verification", only: [:show] scope module: :verification do diff --git a/db/migrate/20150928115005_create_notifications.rb b/db/migrate/20150928115005_create_notifications.rb new file mode 100644 index 000000000..42bbbf72e --- /dev/null +++ b/db/migrate/20150928115005_create_notifications.rb @@ -0,0 +1,9 @@ +class CreateNotifications < ActiveRecord::Migration + def change + create_table :notifications do |t| + t.belongs_to :user, index: true, foreign_key: true + t.belongs_to :activity, index: true, foreign_key: true + t.boolean :read, default: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 16b50e4e9..cb1c5aa65 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -204,6 +204,17 @@ ActiveRecord::Schema.define(version: 20151215165824) do add_index "moderators", ["user_id"], name: "index_moderators_on_user_id", using: :btree + create_table "notifications", force: :cascade do |t| + t.integer "user_id" + t.integer "activity_id" + t.boolean "read", default: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "notifications", ["activity_id"], name: "index_notifications_on_activity_id", using: :btree + add_index "notifications", ["user_id"], name: "index_notifications_on_user_id", using: :btree + create_table "organizations", force: :cascade do |t| t.integer "user_id" t.string "name", limit: 60 @@ -405,5 +416,7 @@ ActiveRecord::Schema.define(version: 20151215165824) do add_foreign_key "identities", "users" add_foreign_key "locks", "users" add_foreign_key "moderators", "users" + add_foreign_key "notifications", "activities" + add_foreign_key "notifications", "users" add_foreign_key "organizations", "users" end diff --git a/spec/controllers/notifications_controller_spec.rb b/spec/controllers/notifications_controller_spec.rb new file mode 100644 index 000000000..c4ff5f247 --- /dev/null +++ b/spec/controllers/notifications_controller_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +describe NotificationsController do + + describe "#index" do + let(:user) { create :user } + let(:notification) { create :notification, user: user } + + it "assigns @notifications" do + sign_in user + + get :index, debate: { title: 'A sample debate', description: 'this is a sample debate', terms_of_service: 1 } + expect(assigns(:notifications)).to eq user.notifications.unread.recent.for_render + end + end +end diff --git a/spec/factories.rb b/spec/factories.rb index 2c20000ca..8cdd3a3e4 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -290,4 +290,10 @@ FactoryGirl.define do sequence(:track_id) { |n| "#{n}" } end + factory :notification do + association :user, factory: :user + association :activity, factory: :activity + read false + end + end diff --git a/spec/features/notifications_spec.rb b/spec/features/notifications_spec.rb new file mode 100644 index 000000000..4b5b48f0e --- /dev/null +++ b/spec/features/notifications_spec.rb @@ -0,0 +1,97 @@ +require 'rails_helper' + +feature "Notifications" do + let(:author) { create :user } + let(:user) { create :user } + let(:debate) { create :debate, author: author } + + scenario "User commented on my debate", :js do + login_as user + visit debate_path debate + + fill_in "comment-body-debate_#{debate.id}", with: "I commented on your debate" + click_button "Publish comment" + within "#comments" do + expect(page).to have_content "I commented on your debate" + end + + logout + login_as author + visit root_path + expect(page).to have_xpath "//a[@class='with_notifications' and @href='#{notifications_path}' and text()='Notificaciones']" + + click_link "Notificaciones" + expect(page).to have_content user.username + expect(page).to have_content I18n.t("comments.notifications.commented_on_your_debate") + expect(page).to_not have_content I18n.t("comments.notifications.replied_to_your_comment") + expect(page).to have_link debate.title, href: debate_path(debate) + end + + scenario "User replied to my comment", :js do + comment = create :comment, commentable: debate, user: author + login_as user + visit debate_path debate + + click_link "Reply" + within "#js-comment-form-comment_#{comment.id}" do + fill_in "comment-body-comment_#{comment.id}", with: "I replied to your comment" + click_button "Publish reply" + end + + within "#comment_#{comment.id}" do + expect(page).to have_content "I replied to your comment" + end + + logout + login_as author + visit root_path + expect(page).to have_xpath "//a[@class='with_notifications' and @href='#{notifications_path}' and text()='Notificaciones']" + + visit notifications_path + expect(page).to have_content user.username + expect(page).to have_content I18n.t("comments.notifications.replied_to_your_comment") + expect(page).to_not have_content I18n.t("comments.notifications.commented_on_your_debate") + expect(page).to have_link debate.title, href: debate_path(debate) + end + + scenario "Author commented on his own debate", :js do + login_as author + visit debate_path debate + + fill_in "comment-body-debate_#{debate.id}", with: "I commented on my own debate" + click_button "Publish comment" + within "#comments" do + expect(page).to have_content "I commented on my own debate" + end + expect(page).to have_xpath "//a[@class='without_notifications' and @href='#{notifications_path}' and text()='Notificaciones']" + + click_link "Notificaciones" + expect(page).to_not have_content user.username + expect(page).to_not have_content I18n.t("comments.notifications.commented_on_your_debate") + expect(page).to_not have_content I18n.t("comments.notifications.replied_to_your_comment") + expect(page).to_not have_link debate.title, href: debate_path(debate) + end + + scenario "Author replied to his own comment", :js do + comment = create :comment, commentable: debate, user: author + login_as author + visit debate_path debate + + click_link "Reply" + within "#js-comment-form-comment_#{comment.id}" do + fill_in "comment-body-comment_#{comment.id}", with: "I replied to my own comment" + click_button "Publish reply" + end + + within "#comment_#{comment.id}" do + expect(page).to have_content "I replied to my own comment" + end + expect(page).to have_xpath "//a[@class='without_notifications' and @href='#{notifications_path}' and text()='Notificaciones']" + + visit notifications_path + expect(page).to_not have_content user.username + expect(page).to_not have_content I18n.t("comments.notifications.replied_to_your_comment") + expect(page).to_not have_content I18n.t("comments.notifications.commented_on_your_debate") + expect(page).to_not have_link debate.title, href: debate_path(debate) + end +end diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb new file mode 100644 index 000000000..120369bf4 --- /dev/null +++ b/spec/helpers/notifications_helper_spec.rb @@ -0,0 +1,48 @@ +require 'rails_helper' + +describe NotificationsHelper do + + describe "#notification_text_for" do + let(:comment_activity) { create :activity, action: "debate_comment" } + let(:reply_activity) { create :activity, action: "comment_reply" } + + context "when action was comment on a debate" do + it "returns 'commented_on_your_debate' locale text" do + notification = create :notification, activity: comment_activity + expect(notification_text_for(notification)).to eq t("comments.notifications.commented_on_your_debate") + end + end + + context "when action was comment on a debate" do + it "returns 'replied_to_your_comment' locale text" do + notification = create :notification, activity: reply_activity + expect(notification_text_for(notification)).to eq t("comments.notifications.replied_to_your_comment") + end + end + end + + describe "#notifications_class_for" do + let(:user) { create :user } + + context "when user doesn't have any notification" do + it "returns class 'without_notifications'" do + expect(notifications_class_for(user)).to eq "without_notifications" + end + end + + context "when user doesn't have unread notifications" do + it "returns class 'without_notifications'" do + notification = create :notification, user: user, read: true + expect(notifications_class_for(user)).to eq "without_notifications" + end + end + + context "when user has unread notifications" do + it "returns class 'with_notifications'" do + notification = create :notification, user: user + expect(notifications_class_for(user)).to eq "with_notifications" + end + end + end + +end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb new file mode 100644 index 000000000..ee5f00e6d --- /dev/null +++ b/spec/models/notification_spec.rb @@ -0,0 +1,44 @@ +require 'rails_helper' + +describe Notification do + + describe "#unread (scope)" do + it "returns only unread notifications" do + unread_notification = create :notification + read_notification = create :notification, read: true + + unread_notifications = Notification.unread + expect(unread_notifications.size).to be 1 + expect(unread_notifications.first).to eq unread_notification + end + end + + describe "#recent (scope)" do + it "returns notifications sorted by id descendant" do + old_notification = create :notification + new_notification = create :notification + + sorted_notifications = Notification.recent + expect(sorted_notifications.size).to be 2 + expect(sorted_notifications.first).to eq new_notification + expect(sorted_notifications.last).to eq old_notification + end + end + + describe "#for_render (scope)" do + it "returns notifications with including activity, user and trackable info" do + expect(Notification).to receive(:includes).with(activity: [:user, :trackable]).exactly(:once) + Notification.for_render + end + end + + describe "#timestamp" do + it "returns the timestamp of the trackable object" do + comment = create :comment + activity = create :activity, trackable: comment + notification = create :notification, activity: activity + + expect(notification.timestamp).to eq comment.created_at + end + end +end From 9a5f525dc95fd974a025ad1807c9d9f524d2ad55 Mon Sep 17 00:00:00 2001 From: Julian Herrero Date: Tue, 6 Oct 2015 20:48:46 +0200 Subject: [PATCH 03/64] marcar notificaciones como leidas despues de ser vistas --- app/controllers/notifications_controller.rb | 1 + app/models/notification.rb | 4 ++++ spec/controllers/notifications_controller_spec.rb | 15 ++++++++++----- spec/models/notification_spec.rb | 10 ++++++++++ 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 2fb715287..199893bd7 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -4,6 +4,7 @@ class NotificationsController < ApplicationController def index @notifications = current_user.notifications.unread.recent.for_render + @notifications.each { |notification| notification.mark_as_read! } end end diff --git a/app/models/notification.rb b/app/models/notification.rb index aff5c38fa..0f387f61f 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -9,4 +9,8 @@ class Notification < ActiveRecord::Base def timestamp activity.trackable.created_at end + + def mark_as_read! + update_attribute :read, true + end end diff --git a/spec/controllers/notifications_controller_spec.rb b/spec/controllers/notifications_controller_spec.rb index c4ff5f247..fa6104a9c 100644 --- a/spec/controllers/notifications_controller_spec.rb +++ b/spec/controllers/notifications_controller_spec.rb @@ -4,13 +4,18 @@ describe NotificationsController do describe "#index" do let(:user) { create :user } - let(:notification) { create :notification, user: user } - it "assigns @notifications" do + it "mark all notifications as read" do + notifications = [create(:notification, user: user), create(:notification, user: user)] + Notification.all.each do |notification| + expect(notification.read).to be false + end + sign_in user - - get :index, debate: { title: 'A sample debate', description: 'this is a sample debate', terms_of_service: 1 } - expect(assigns(:notifications)).to eq user.notifications.unread.recent.for_render + get :index + Notification.all.each do |notification| + expect(notification.read).to be true + end end end end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index ee5f00e6d..8d5e3dd97 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -41,4 +41,14 @@ describe Notification do expect(notification.timestamp).to eq comment.created_at end end + + describe "#mark_as_read" do + it "set up read flag to true" do + notification = create :notification + expect(notification.read).to be false + + notification.mark_as_read! + expect(notification.read).to be true + end + end end From 7a8ffe2d4a9e692d3e1e244ff296b57213475f9b Mon Sep 17 00:00:00 2001 From: Julian Herrero Date: Tue, 6 Oct 2015 20:56:11 +0200 Subject: [PATCH 04/64] cleanup --- app/views/notifications/index.html.erb | 2 +- config/locales/en.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index 222907c7e..ab4351c50 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -2,7 +2,7 @@
      <% @notifications.each do |notification| %>
    • -  •  +  •  <%= notification.activity.username %> <%= notification_text_for(notification) %> <%= link_to notification.activity.trackable.debate.title, notification.activity.trackable.debate %>
    • diff --git a/config/locales/en.yml b/config/locales/en.yml index a6a4d55bc..35af31bbc 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -268,7 +268,7 @@ en: one: "1 vote" other: "%{count} votes" notifications: - commented_on_your_debate: "commented on yout debate" + commented_on_your_debate: "commented on your debate" replied_to_your_comment: "replied to your comment on" comments_helper: comment_link: "Comment" From b5e9113718d2f6fd143e615058f1eee8beb3cc05 Mon Sep 17 00:00:00 2001 From: rgarcia Date: Wed, 6 Jan 2016 12:33:37 +0100 Subject: [PATCH 05/64] merges activities into notifications --- app/controllers/application_controller.rb | 17 ----------------- app/controllers/comments_controller.rb | 11 ++++++++++- app/controllers/notifications_controller.rb | 3 +-- app/helpers/notifications_helper.rb | 11 +++++------ app/models/comment.rb | 4 ++++ app/models/notification.rb | 15 +++++++++------ app/views/notifications/index.html.erb | 4 ++-- db/migrate/20150928075646_create_activities.rb | 9 --------- ...170113_merge_activities_and_notifications.rb | 10 ++++++++++ db/schema.rb | 13 ++++--------- spec/factories.rb | 5 ++--- spec/helpers/notifications_helper_spec.rb | 17 +++++++++-------- spec/models/comment_spec.rb | 15 +++++++++++++++ spec/models/notification_spec.rb | 16 ++++++++++++---- 14 files changed, 83 insertions(+), 67 deletions(-) delete mode 100644 db/migrate/20150928075646_create_activities.rb create mode 100644 db/migrate/20160105170113_merge_activities_and_notifications.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 312617c4e..5150f97ec 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -109,21 +109,4 @@ class ApplicationController < ActionController::Base end end - def track_activity(trackable) - if trackable.is_a? Comment - action = trackable.root? ? "debate_comment" : "comment_reply" - activity = current_user.activities.create! action: action, trackable: trackable - add_notifications_for activity - end - end - - def add_notifications_for(activity) - case activity.action - when "debate_comment" - author = activity.trackable.debate.author - when "comment_reply" - author = activity.trackable.parent.author - end - author.notifications.create!(activity: activity) unless activity.made_by? author - end end diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index e05be448a..c30ddbaf2 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -9,7 +9,7 @@ class CommentsController < ApplicationController def create if @comment.save CommentNotifier.new(comment: @comment).process - track_activity @comment + add_notification @comment else render :new end @@ -63,4 +63,13 @@ class CommentsController < ApplicationController ["1", true].include?(comment_params[:as_moderator]) && can?(:comment_as_moderator, @commentable) end + def add_notification(comment) + if comment.reply? + author = comment.parent.author + else + author = comment.commentable.author + end + author.notifications.create!(notifiable: comment) unless comment.made_by? author + end + end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 199893bd7..8ecb16104 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -3,8 +3,7 @@ class NotificationsController < ApplicationController load_and_authorize_resource class: "User" def index - @notifications = current_user.notifications.unread.recent.for_render - @notifications.each { |notification| notification.mark_as_read! } + @notifications = current_user.notifications.recent.for_render end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index d0e7e71a9..45ef6422f 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,16 +1,15 @@ module NotificationsHelper def notification_text_for(notification) - case notification.activity.action - when "debate_comment" - t("comments.notifications.commented_on_your_debate") - when "comment_reply" - t("comments.notifications.replied_to_your_comment") + if notification.notifiable.reply? + t("comments.notifications.replied_to_your_comment") + else + t("comments.notifications.commented_on_your_debate") end end def notifications_class_for(user) - user.notifications.unread.count > 0 ? "with_notifications" : "without_notifications" + user.notifications.count > 0 ? "with_notifications" : "without_notifications" end end diff --git a/app/models/comment.rb b/app/models/comment.rb index 3771af84a..44e16b729 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -90,6 +90,10 @@ class Comment < ActiveRecord::Base !root? end + def made_by?(user) + self.user == user + end + def call_after_commented self.commentable.try(:after_commented) end diff --git a/app/models/notification.rb b/app/models/notification.rb index 0f387f61f..63f44dedc 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -1,16 +1,19 @@ class Notification < ActiveRecord::Base belongs_to :user - belongs_to :activity + belongs_to :notifiable, polymorphic: true - scope :unread, -> { where(read: false) } + scope :unread, -> { all } scope :recent, -> { order(id: :desc) } - scope :for_render, -> { includes(activity: [:user, :trackable]) } + scope :for_render, -> { includes(notifiable: [:user]) } + + def username + notifiable.user.username + end def timestamp - activity.trackable.created_at + notifiable.created_at end def mark_as_read! - update_attribute :read, true end -end +end \ No newline at end of file diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index ab4351c50..0155f0521 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -3,8 +3,8 @@ <% @notifications.each do |notification| %>
    •  •  - <%= notification.activity.username %> <%= notification_text_for(notification) %> - <%= link_to notification.activity.trackable.debate.title, notification.activity.trackable.debate %> + <%= notification.username %> <%= notification_text_for(notification) %> + <%= link_to notification.notifiable.commentable.title, notification.notifiable.commentable %>
    • <% end %>
    diff --git a/db/migrate/20150928075646_create_activities.rb b/db/migrate/20150928075646_create_activities.rb deleted file mode 100644 index 89b0edcbc..000000000 --- a/db/migrate/20150928075646_create_activities.rb +++ /dev/null @@ -1,9 +0,0 @@ -class CreateActivities < ActiveRecord::Migration - def change - create_table :activities do |t| - t.belongs_to :user, index: true, foreign_key: true - t.string :action - t.belongs_to :trackable, polymorphic: true - end - end -end diff --git a/db/migrate/20160105170113_merge_activities_and_notifications.rb b/db/migrate/20160105170113_merge_activities_and_notifications.rb new file mode 100644 index 000000000..fac5b213d --- /dev/null +++ b/db/migrate/20160105170113_merge_activities_and_notifications.rb @@ -0,0 +1,10 @@ +class MergeActivitiesAndNotifications < ActiveRecord::Migration + def change + change_table :notifications do |t| + t.remove :read + t.remove :activity_id + t.references :notifiable, polymorphic: true + end + end + +end diff --git a/db/schema.rb b/db/schema.rb index cb1c5aa65..35aa4b68c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20151215165824) do +ActiveRecord::Schema.define(version: 20160105170113) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -205,14 +205,11 @@ ActiveRecord::Schema.define(version: 20151215165824) do add_index "moderators", ["user_id"], name: "index_moderators_on_user_id", using: :btree create_table "notifications", force: :cascade do |t| - t.integer "user_id" - t.integer "activity_id" - t.boolean "read", default: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.integer "user_id" + t.integer "notifiable_id" + t.string "notifiable_type" end - add_index "notifications", ["activity_id"], name: "index_notifications_on_activity_id", using: :btree add_index "notifications", ["user_id"], name: "index_notifications_on_user_id", using: :btree create_table "organizations", force: :cascade do |t| @@ -407,7 +404,6 @@ ActiveRecord::Schema.define(version: 20151215165824) do add_index "votes", ["votable_id", "votable_type", "vote_scope"], name: "index_votes_on_votable_id_and_votable_type_and_vote_scope", using: :btree add_index "votes", ["voter_id", "voter_type", "vote_scope"], name: "index_votes_on_voter_id_and_voter_type_and_vote_scope", using: :btree - add_foreign_key "activities", "users" add_foreign_key "administrators", "users" add_foreign_key "annotations", "legislations" add_foreign_key "annotations", "users" @@ -416,7 +412,6 @@ ActiveRecord::Schema.define(version: 20151215165824) do add_foreign_key "identities", "users" add_foreign_key "locks", "users" add_foreign_key "moderators", "users" - add_foreign_key "notifications", "activities" add_foreign_key "notifications", "users" add_foreign_key "organizations", "users" end diff --git a/spec/factories.rb b/spec/factories.rb index 8cdd3a3e4..d48aba942 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -291,9 +291,8 @@ FactoryGirl.define do end factory :notification do - association :user, factory: :user - association :activity, factory: :activity - read false + user + association :notifiable, factory: :comment end end diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb index 120369bf4..dd5cd1b78 100644 --- a/spec/helpers/notifications_helper_spec.rb +++ b/spec/helpers/notifications_helper_spec.rb @@ -3,20 +3,21 @@ require 'rails_helper' describe NotificationsHelper do describe "#notification_text_for" do - let(:comment_activity) { create :activity, action: "debate_comment" } - let(:reply_activity) { create :activity, action: "comment_reply" } + let(:debate) { create :debate } + let(:debate_comment) { create :comment, commentable: debate } + let(:comment_reply) { create :comment, commentable: debate, parent: debate_comment } context "when action was comment on a debate" do - it "returns 'commented_on_your_debate' locale text" do - notification = create :notification, activity: comment_activity - expect(notification_text_for(notification)).to eq t("comments.notifications.commented_on_your_debate") + it "returns correct text when someone comments on your debate" do + notification = create :notification, notifiable: debate_comment + expect(notification_text_for(notification)).to eq "commented on your debate" end end context "when action was comment on a debate" do - it "returns 'replied_to_your_comment' locale text" do - notification = create :notification, activity: reply_activity - expect(notification_text_for(notification)).to eq t("comments.notifications.replied_to_your_comment") + it "returns correct text when someone replies to your comment" do + notification = create :notification, notifiable: comment_reply + expect(notification_text_for(notification)).to eq "replied to your comment on" end end end diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index 11a02400a..684de6438 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -128,4 +128,19 @@ describe Comment do expect(Comment.not_as_admin_or_moderator.first).to eq(comment1) end end + + describe "#made_by?" do + let(:author) { create :user } + let(:comment) { create :comment, user: author } + + it "returns true if comment was made by user" do + expect(comment.made_by?(author)).to be true + end + + it "returns false if comment was not made by user" do + not_author = create :user + expect(comment.made_by?(not_author)).to be false + end + end + end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index 8d5e3dd97..d93dcf6f3 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -26,8 +26,8 @@ describe Notification do end describe "#for_render (scope)" do - it "returns notifications with including activity, user and trackable info" do - expect(Notification).to receive(:includes).with(activity: [:user, :trackable]).exactly(:once) + it "returns notifications including notifiable and user" do + expect(Notification).to receive(:includes).with(notifiable: [:user]).exactly(:once) Notification.for_render end end @@ -35,8 +35,7 @@ describe Notification do describe "#timestamp" do it "returns the timestamp of the trackable object" do comment = create :comment - activity = create :activity, trackable: comment - notification = create :notification, activity: activity + notification = create :notification, notifiable: comment expect(notification.timestamp).to eq comment.created_at end @@ -51,4 +50,13 @@ describe Notification do expect(notification.read).to be true end end + + describe "#username" do + it "returns the username of the activity's author" do + comment = create :comment + notification = create :notification, notifiable: comment + expect(notification.username).to eq comment.author.username + end + end + end From e2f419e625f1996cbe14e461ce487e221ca5f2ce Mon Sep 17 00:00:00 2001 From: rgarcia Date: Thu, 7 Jan 2016 12:02:01 +0100 Subject: [PATCH 06/64] destroy notifications when marked as read --- app/controllers/notifications_controller.rb | 21 ++++++- app/helpers/notifications_helper.rb | 8 +-- app/models/notification.rb | 3 +- .../notifications/_notification.html.erb | 8 +++ app/views/notifications/index.html.erb | 25 ++++---- config/locales/en.yml | 9 ++- config/locales/es.yml | 9 ++- config/routes.rb | 4 +- .../notifications_controller_spec.rb | 21 ------- spec/features/notifications_spec.rb | 63 ++++++++++++++----- spec/helpers/notifications_helper_spec.rb | 17 ++--- spec/models/notification_spec.rb | 16 ++--- 12 files changed, 120 insertions(+), 84 deletions(-) create mode 100644 app/views/notifications/_notification.html.erb delete mode 100644 spec/controllers/notifications_controller_spec.rb diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 8ecb16104..3820d7e36 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -1,9 +1,26 @@ class NotificationsController < ApplicationController before_action :authenticate_user! - load_and_authorize_resource class: "User" + after_action :mark_as_read, only: :show + skip_authorization_check def index - @notifications = current_user.notifications.recent.for_render + @notifications = current_user.notifications.unread.recent.for_render end + def show + @notification = current_user.notifications.find(params[:id]) + redirect_to url_for(@notification.notifiable.commentable) + end + + def mark_all_as_read + current_user.notifications.each { |notification| notification.mark_as_read } + redirect_to notifications_path + end + + private + + def mark_as_read + @notification.mark_as_read + end + end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 45ef6422f..1945d7c5b 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,11 +1,7 @@ module NotificationsHelper - def notification_text_for(notification) - if notification.notifiable.reply? - t("comments.notifications.replied_to_your_comment") - else - t("comments.notifications.commented_on_your_debate") - end + def notification_action(notification) + notification.notifiable.reply? ? "replied_to_your_comment" : "commented_on_your_debate" end def notifications_class_for(user) diff --git a/app/models/notification.rb b/app/models/notification.rb index 63f44dedc..e22e68ad2 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -14,6 +14,7 @@ class Notification < ActiveRecord::Base notifiable.created_at end - def mark_as_read! + def mark_as_read + self.destroy end end \ No newline at end of file diff --git a/app/views/notifications/_notification.html.erb b/app/views/notifications/_notification.html.erb new file mode 100644 index 000000000..313a5458c --- /dev/null +++ b/app/views/notifications/_notification.html.erb @@ -0,0 +1,8 @@ +
  • + <%= link_to notification do %> +  •  + <%= notification.username %> + <%= t("notifications.index.#{notification_action(notification)}") %> + <%= notification.notifiable.commentable.title %> + <% end %> +
  • \ No newline at end of file diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index 0155f0521..5eaebefb9 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -1,11 +1,14 @@ -
    -
      - <% @notifications.each do |notification| %> -
    • -  •  - <%= notification.username %> <%= notification_text_for(notification) %> - <%= link_to notification.notifiable.commentable.title, notification.notifiable.commentable %> -
    • - <% end %> -
    -
    +<% if @notifications.empty? %> +
    <%= t("notifications.index.empty_notifications") %>
    +<% else %> +
    +
      + <%= render @notifications %> +
    + +
    + <%= link_to t("notifications.index.mark_all_as_read"), + mark_all_as_read_notifications_path, method: :put %> +
    +
    +<% end %> \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 35af31bbc..683db0454 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -267,9 +267,6 @@ en: zero: "No votes" one: "1 vote" other: "%{count} votes" - notifications: - commented_on_your_debate: "commented on your debate" - replied_to_your_comment: "replied to your comment on" comments_helper: comment_link: "Comment" comment_button: "Publish comment" @@ -313,6 +310,12 @@ en: user_permission_votes: "Participate on final voting" user_permission_verify: "To perform all the actions verify your account." user_permission_verify_info: "* Only for users on Madrid City Census." + notifications: + index: + mark_all_as_read: "Mark all as read" + empty_notifications: "There are no new notifications." + commented_on_your_debate: "commented on your debate" + replied_to_your_comment: "replied to your comment on" simple_captcha: placeholder: "Enter the text from the image" label: "Enter the text from the image in the box below" diff --git a/config/locales/es.yml b/config/locales/es.yml index c10d8a493..f6fd907de 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -267,9 +267,6 @@ es: zero: "Sin votos" one: "1 voto" other: "%{count} votos" - notifications: - commented_on_your_debate: "ha comentado en tu debate" - replied_to_your_comment: "ha respondido a tu comentario en" comments_helper: comment_link: "Comentar" comment_button: "Publicar comentario" @@ -313,6 +310,12 @@ es: user_permission_votes: "Participar en las votaciones finales*" user_permission_verify: "Para poder realizar todas las acciones verifica tu cuenta." user_permission_verify_info: "* Sólo usuarios empadronados en el municipio de Madrid." + notifications: + index: + mark_all_as_read: "Marcar todas como leídas" + empty_notifications: "No hay notificaciones nuevas." + commented_on_your_debate: "ha comentado en tu debate" + replied_to_your_comment: "ha respondido a tu comentario en" simple_captcha: placeholder: "Introduce el texto de la imagen" label: "Introduce el texto de la imagen en la siguiente caja" diff --git a/config/routes.rb b/config/routes.rb index 681eff89b..29b63e91b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -75,7 +75,9 @@ Rails.application.routes.draw do collection { get :erase } end - resources :notifications, only: :index + resources :notifications, only: [:index, :show] do + collection { put :mark_all_as_read } + end resource :verification, controller: "verification", only: [:show] diff --git a/spec/controllers/notifications_controller_spec.rb b/spec/controllers/notifications_controller_spec.rb deleted file mode 100644 index fa6104a9c..000000000 --- a/spec/controllers/notifications_controller_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'rails_helper' - -describe NotificationsController do - - describe "#index" do - let(:user) { create :user } - - it "mark all notifications as read" do - notifications = [create(:notification, user: user), create(:notification, user: user)] - Notification.all.each do |notification| - expect(notification.read).to be false - end - - sign_in user - get :index - Notification.all.each do |notification| - expect(notification.read).to be true - end - end - end -end diff --git a/spec/features/notifications_spec.rb b/spec/features/notifications_spec.rb index 4b5b48f0e..22e4b320d 100644 --- a/spec/features/notifications_spec.rb +++ b/spec/features/notifications_spec.rb @@ -21,10 +21,10 @@ feature "Notifications" do expect(page).to have_xpath "//a[@class='with_notifications' and @href='#{notifications_path}' and text()='Notificaciones']" click_link "Notificaciones" + expect(page).to have_css ".notification", count: 1 expect(page).to have_content user.username - expect(page).to have_content I18n.t("comments.notifications.commented_on_your_debate") - expect(page).to_not have_content I18n.t("comments.notifications.replied_to_your_comment") - expect(page).to have_link debate.title, href: debate_path(debate) + expect(page).to have_content "commented on your debate" + expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" end scenario "User replied to my comment", :js do @@ -48,10 +48,10 @@ feature "Notifications" do expect(page).to have_xpath "//a[@class='with_notifications' and @href='#{notifications_path}' and text()='Notificaciones']" visit notifications_path + expect(page).to have_css ".notification", count: 1 expect(page).to have_content user.username - expect(page).to have_content I18n.t("comments.notifications.replied_to_your_comment") - expect(page).to_not have_content I18n.t("comments.notifications.commented_on_your_debate") - expect(page).to have_link debate.title, href: debate_path(debate) + expect(page).to have_content "replied to your comment on" + expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" end scenario "Author commented on his own debate", :js do @@ -66,10 +66,7 @@ feature "Notifications" do expect(page).to have_xpath "//a[@class='without_notifications' and @href='#{notifications_path}' and text()='Notificaciones']" click_link "Notificaciones" - expect(page).to_not have_content user.username - expect(page).to_not have_content I18n.t("comments.notifications.commented_on_your_debate") - expect(page).to_not have_content I18n.t("comments.notifications.replied_to_your_comment") - expect(page).to_not have_link debate.title, href: debate_path(debate) + expect(page).to have_css ".notification", count: 0 end scenario "Author replied to his own comment", :js do @@ -89,9 +86,47 @@ feature "Notifications" do expect(page).to have_xpath "//a[@class='without_notifications' and @href='#{notifications_path}' and text()='Notificaciones']" visit notifications_path - expect(page).to_not have_content user.username - expect(page).to_not have_content I18n.t("comments.notifications.replied_to_your_comment") - expect(page).to_not have_content I18n.t("comments.notifications.commented_on_your_debate") - expect(page).to_not have_link debate.title, href: debate_path(debate) + expect(page).to have_css ".notification", count: 0 end + + context "mark as read" do + + scenario "mark a single notification as read" do + user = create :user + notification = create :notification, user: user + + login_as user + visit notifications_path + + expect(page).to have_css ".notification", count: 1 + + first(".notification a").click + visit notifications_path + + expect(page).to have_css ".notification", count: 0 + end + + scenario "mark all notifications as read" do + user = create :user + 2.times { create :notification, user: user } + + login_as user + visit notifications_path + + expect(page).to have_css ".notification", count: 2 + click_link "Mark all as read" + + expect(page).to have_css ".notification", count: 0 + expect(current_path).to eq(notifications_path) + end + + end + + scenario "no notifications" do + login_as user + visit notifications_path + + expect(page).to have_content "There are no new notifications" + end + end diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb index dd5cd1b78..fa378b2ec 100644 --- a/spec/helpers/notifications_helper_spec.rb +++ b/spec/helpers/notifications_helper_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' describe NotificationsHelper do - describe "#notification_text_for" do + describe "#notification_action" do let(:debate) { create :debate } let(:debate_comment) { create :comment, commentable: debate } let(:comment_reply) { create :comment, commentable: debate, parent: debate_comment } @@ -10,14 +10,14 @@ describe NotificationsHelper do context "when action was comment on a debate" do it "returns correct text when someone comments on your debate" do notification = create :notification, notifiable: debate_comment - expect(notification_text_for(notification)).to eq "commented on your debate" + expect(notification_action(notification)).to eq "commented_on_your_debate" end end context "when action was comment on a debate" do it "returns correct text when someone replies to your comment" do notification = create :notification, notifiable: comment_reply - expect(notification_text_for(notification)).to eq "replied to your comment on" + expect(notification_action(notification)).to eq "replied_to_your_comment" end end end @@ -25,20 +25,13 @@ describe NotificationsHelper do describe "#notifications_class_for" do let(:user) { create :user } - context "when user doesn't have any notification" do + context "when user doesn't have notifications" do it "returns class 'without_notifications'" do expect(notifications_class_for(user)).to eq "without_notifications" end end - context "when user doesn't have unread notifications" do - it "returns class 'without_notifications'" do - notification = create :notification, user: user, read: true - expect(notifications_class_for(user)).to eq "without_notifications" - end - end - - context "when user has unread notifications" do + context "when user has notifications" do it "returns class 'with_notifications'" do notification = create :notification, user: user expect(notifications_class_for(user)).to eq "with_notifications" diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index d93dcf6f3..0b8354e83 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -4,12 +4,8 @@ describe Notification do describe "#unread (scope)" do it "returns only unread notifications" do - unread_notification = create :notification - read_notification = create :notification, read: true - - unread_notifications = Notification.unread - expect(unread_notifications.size).to be 1 - expect(unread_notifications.first).to eq unread_notification + 2.times { create :notification } + expect(Notification.unread.size).to be 2 end end @@ -42,12 +38,12 @@ describe Notification do end describe "#mark_as_read" do - it "set up read flag to true" do + it "destroys notification" do notification = create :notification - expect(notification.read).to be false + expect(Notification.unread.size).to eq 1 - notification.mark_as_read! - expect(notification.read).to be true + notification.mark_as_read + expect(Notification.unread.size).to eq 0 end end From 816a95b7b72df3a041536cb8e9dc43864b5e6efc Mon Sep 17 00:00:00 2001 From: rgarcia Date: Thu, 7 Jan 2016 14:19:05 +0100 Subject: [PATCH 07/64] adds comment show view --- app/controllers/comments_controller.rb | 7 ++++++- app/helpers/comments_helper.rb | 7 +++++-- app/models/abilities/everyone.rb | 1 + app/views/comments/_comment.html.erb | 13 +++++++------ app/views/comments/show.html.erb | 12 ++++++++++++ config/locales/en.yml | 2 ++ config/locales/es.yml | 2 ++ config/routes.rb | 2 +- spec/features/comments/debates_spec.rb | 17 ++++++++++++++++- spec/features/comments/proposals_spec.rb | 15 +++++++++++++++ spec/models/abilities/everyone_spec.rb | 2 ++ 11 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 app/views/comments/show.html.erb diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 35406faf0..050962414 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -1,5 +1,5 @@ class CommentsController < ApplicationController - before_action :authenticate_user! + before_action :authenticate_user!, only: :create before_action :load_commentable, only: :create before_action :build_comment, only: :create @@ -14,6 +14,11 @@ class CommentsController < ApplicationController end end + def show + @comment = Comment.find(params[:id]) + set_comment_flags(@comment.subtree) + end + def vote @comment.vote_by(voter: current_user, vote: params[:value]) respond_with @comment diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb index 9ac29e4da..9d9d08075 100644 --- a/app/helpers/comments_helper.rb +++ b/app/helpers/comments_helper.rb @@ -13,8 +13,11 @@ module CommentsHelper end def child_comments_of(parent) - return [] unless @comment_tree - @comment_tree.children_of(parent) + if @comment_tree.present? + @comment_tree.ordered_children_of(parent) + else + parent.children + end end def user_level_class(comment) diff --git a/app/models/abilities/everyone.rb b/app/models/abilities/everyone.rb index d6b2a6d57..122d5db2a 100644 --- a/app/models/abilities/everyone.rb +++ b/app/models/abilities/everyone.rb @@ -5,6 +5,7 @@ module Abilities def initialize(user) can :read, Debate can :read, Proposal + can :read, Comment can :read, Legislation can :read, User can [:search, :read], Annotation diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index 9178f0b9b..44e15362b 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -1,9 +1,9 @@ -<% cache [locale_and_user_status(comment), comment, commentable_cache_key(@commentable), comment.author, (@comment_flags[comment.id] if @comment_flags)] do %> +<% cache [locale_and_user_status(comment), comment, commentable_cache_key(comment.commentable), comment.author, (@comment_flags[comment.id] if @comment_flags)] do %>
    <% if comment.hidden? || comment.user.hidden? %> - <% if child_comments_of(comment).size > 0 %> + <% if comment.children.size > 0 %>

    <%= t("comments.comment.deleted") %>

    @@ -49,7 +49,7 @@ <%= t("shared.collective") %> <% end %> - <% if comment.user_id == @commentable.author_id %> + <% if comment.user_id == comment.commentable.author_id %>  •  <%= t("comments.comment.author") %> @@ -63,7 +63,7 @@
    + <%= comment_author_class comment, comment.commentable.author_id %>"> <%= simple_format text_with_links comment.body %>
    @@ -72,7 +72,7 @@
    - <%= t("comments.comment.responses", count: child_comments_of(comment).size) %> + <%= t("comments.comment.responses", count: comment.children.size) %> <% if user_signed_in? %>  |  @@ -81,11 +81,12 @@ <%= render 'comments/actions', comment: comment %> - <%= render 'comments/form', {commentable: @commentable, parent_id: comment.id, toggeable: true} %> + <%= render 'comments/form', {commentable: comment.commentable, parent_id: comment.id, toggeable: true} %> <% end %>
    <% end %> +
    <% child_comments_of(comment).each do |child| %> <%= render 'comments/comment', comment: child %> diff --git a/app/views/comments/show.html.erb b/app/views/comments/show.html.erb new file mode 100644 index 000000000..b0c3dfb3f --- /dev/null +++ b/app/views/comments/show.html.erb @@ -0,0 +1,12 @@ +
    + <%= link_to t("comments.show.return_to_commentable") + @comment.commentable.title, + @comment.commentable %> +
    + +
    +
    +
    + <%= render @comment %> +
    +
    +
    \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 3ee698c91..57a9fb92e 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -244,6 +244,8 @@ en: form: submit_button: "Save changes" comments: + show: + return_to_commentable: "Go back to " select_order: "Sort by" orders: most_voted: "Most voted" diff --git a/config/locales/es.yml b/config/locales/es.yml index 46bb3b88b..65eb1eb8b 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -244,6 +244,8 @@ es: form: submit_button: "Guardar cambios" comments: + show: + return_to_commentable: "Volver a " select_order: "Ordenar por" orders: most_voted: "Más votados" diff --git a/config/routes.rb b/config/routes.rb index 3472f1c4e..2da32d479 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -53,7 +53,7 @@ Rails.application.routes.draw do end end - resources :comments, only: :create, shallow: true do + resources :comments, only: [:create, :show], shallow: true do member do post :vote put :flag diff --git a/spec/features/comments/debates_spec.rb b/spec/features/comments/debates_spec.rb index 5adfa218c..7d807a6c0 100644 --- a/spec/features/comments/debates_spec.rb +++ b/spec/features/comments/debates_spec.rb @@ -20,6 +20,21 @@ feature 'Commenting debates' do end end + scenario 'Show' do + parent_comment = create(:comment, commentable: debate) + first_child = create(:comment, commentable: debate, parent: parent_comment) + second_child = create(:comment, commentable: debate, parent: parent_comment) + + visit comment_path(parent_comment) + + expect(page).to have_css(".comment", count: 3) + expect(page).to have_content parent_comment.body + expect(page).to have_content first_child.body + expect(page).to have_content second_child.body + + expect(page).to have_link "Go back to #{debate.title}", debate_path(debate) + end + scenario 'Comment order' do c1 = create(:comment, :with_confidence_score, commentable: debate, cached_votes_up: 100, cached_votes_total: 120, created_at: Time.now - 2) c2 = create(:comment, :with_confidence_score, commentable: debate, cached_votes_up: 10, cached_votes_total: 12, created_at: Time.now - 1) @@ -254,7 +269,7 @@ feature 'Commenting debates' do fill_in "comment-body-debate_#{debate.id}", with: 'Testing submit button!' click_button 'Publish comment' - + # The button's text should now be "..." # This should be checked before the Ajax request is finished expect(page).to_not have_button 'Publish comment' diff --git a/spec/features/comments/proposals_spec.rb b/spec/features/comments/proposals_spec.rb index 1c277cf4d..d919f0b51 100644 --- a/spec/features/comments/proposals_spec.rb +++ b/spec/features/comments/proposals_spec.rb @@ -20,6 +20,21 @@ feature 'Commenting proposals' do end end + scenario 'Show' do + parent_comment = create(:comment, commentable: proposal) + first_child = create(:comment, commentable: proposal, parent: parent_comment) + second_child = create(:comment, commentable: proposal, parent: parent_comment) + + visit comment_path(parent_comment) + + expect(page).to have_css(".comment", count: 3) + expect(page).to have_content parent_comment.body + expect(page).to have_content first_child.body + expect(page).to have_content second_child.body + + expect(page).to have_link "Go back to #{proposal.title}", proposal_path(proposal) + end + scenario 'Comment order' do c1 = create(:comment, :with_confidence_score, commentable: proposal, cached_votes_up: 100, cached_votes_total: 120, created_at: Time.now - 2) c2 = create(:comment, :with_confidence_score, commentable: proposal, cached_votes_up: 10, cached_votes_total: 12, created_at: Time.now - 1) diff --git a/spec/models/abilities/everyone_spec.rb b/spec/models/abilities/everyone_spec.rb index 4c532b7a4..dacac25e4 100644 --- a/spec/models/abilities/everyone_spec.rb +++ b/spec/models/abilities/everyone_spec.rb @@ -21,4 +21,6 @@ describe "Abilities::Everyone" do it { should_not be_able_to(:vote, Proposal) } it { should_not be_able_to(:flag, Proposal) } it { should_not be_able_to(:unflag, Proposal) } + + it { should be_able_to(:show, Comment) } end From 35704f2be906a1ef9306dba29c44f6a39d117b7f Mon Sep 17 00:00:00 2001 From: rgarcia Date: Thu, 7 Jan 2016 15:18:45 +0100 Subject: [PATCH 08/64] refactors comment tree --- lib/comment_tree.rb | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/comment_tree.rb b/lib/comment_tree.rb index 8d0e072dd..f2eadb8ab 100644 --- a/lib/comment_tree.rb +++ b/lib/comment_tree.rb @@ -2,24 +2,34 @@ class CommentTree ROOT_COMMENTS_PER_PAGE = 10 - attr_accessor :root_comments, :comments + attr_accessor :root_comments, :comments, :commentable, :page, :order def initialize(commentable, page, order = 'confidence_score') - @root_comments = commentable.comments.roots.send("sort_by_#{order}").page(page).per(ROOT_COMMENTS_PER_PAGE).for_render - - root_descendants = @root_comments.each_with_object([]) do |root, col| - col.concat(Comment.descendants_of(root).send("sort_descendants_by_#{order}").for_render.to_a) - end + @commentable = commentable + @page = page + @order = order @comments = root_comments + root_descendants + end - @comments_by_parent_id = @comments.each_with_object({}) do |comment, col| - (col[comment.parent_id] ||= []) << comment + def root_comments + commentable.comments.roots.send("sort_by_#{order}").page(page).per(ROOT_COMMENTS_PER_PAGE).for_render + end + + def root_descendants + root_comments.each_with_object([]) do |root, array| + array.concat(Comment.descendants_of(root).send("sort_descendants_by_#{order}").for_render.to_a) end end - def children_of(parent) - @comments_by_parent_id[parent.id] || [] + def ordered_children_of(parent) + comments_by_parent_id[parent.id] || [] + end + + def comments_by_parent_id + comments.each_with_object({}) do |comment, array| + (array[comment.parent_id] ||= []) << comment + end end def comment_authors From 9c2b5764319cf51b1811433ab889dfddada8c8e8 Mon Sep 17 00:00:00 2001 From: rgarcia Date: Thu, 7 Jan 2016 15:30:48 +0100 Subject: [PATCH 09/64] fixes specs --- config/i18n-tasks.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index c6393c23a..5668ee2ce 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -125,6 +125,8 @@ ignore_unused: - 'debates.index.search_form.*' - 'proposals.index.orders.*' - 'proposals.index.search_form.*' + - 'notifications.index.commented_on_your_debate' + - 'notifications.index.replied_to_your_comment' - 'helpers.page_entries_info.*' # kaminari - 'views.pagination.*' # kaminari # - '{devise,kaminari,will_paginate}.*' From 09120b6129d4c86d7b2a611aa538070a913d8e39 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Cabeza Date: Fri, 8 Jan 2016 12:44:40 +0100 Subject: [PATCH 10/64] Updates icons font --- app/assets/fonts/icons.eot | Bin 10856 -> 9544 bytes app/assets/fonts/icons.svg | 7 ++- app/assets/fonts/icons.ttf | Bin 10700 -> 9388 bytes app/assets/fonts/icons.woff | Bin 8300 -> 7468 bytes app/assets/stylesheets/icons.scss | 93 +++++++++++++++--------------- 5 files changed, 52 insertions(+), 48 deletions(-) diff --git a/app/assets/fonts/icons.eot b/app/assets/fonts/icons.eot index 3046c50cc222d1c158d1f8748287cb28c02f44be..ced98a9b4790e2f0ffb6ab1792a9671d4d63e209 100644 GIT binary patch delta 1603 zcmZ`(T}&KR6h3$UXLojnWp`$m{ejuPnO$&USzwokR;fWyi9&4yrl4lKg@Upyv?Q&@ zbQ@o^iP+XjYOKZ@pGfRO18IEF_!DE&7-AEm4-Gu*gGMFBH(zQjo>>-(X`P#U@AQK_ga-Sc7=SV05YHplhmUk* zhc_m^0wB*L9xav%^VJ*GF#t+J?&Fh%)AQTgK%@RNt_vq;U!5ui2Co4y3_y5fda`hW zd+UccacvWOZW;+DLw>=3y@=z}rNzowafrsv$qghXX3NEbb$4kLfWD4+v{X1d4`<*e zuJo|#B=!Kxj{sW4IZ@NYT-o`8P~HAjM7Gl*VBj3i0=6BWpMlpfxAu1k3U#UG zC)THV1nD6Dl!O)tLo?_Qffz&~j+>;=Bm*p}@n}rMtsUs51TK)l1O+r`05^ER3o7`) z4+bV$paaWi8O20L`!*FNlo}r{t1NMQd<-ysFP{GzF}0=y$Bc zj6eScdy4&@dy)H*yT@nwOZ>XP2r5m$rqU2gR!b&XyGdhTo?|zIJJ-NKytYKBF`0}|fjj*iS&oZ*2vRD6> zx{aXflO%yB6Pj{f)edNC>z&R9z2OBvP3V8N7@8%v)Wf-4PFWIpo{L2Clptt=K*d5q zBS-+cQRatraPE%AAWvS8aDxFufuuoOhmPF69p-FNiNS|P6`d)^js346}yXbHz zE>-n=k}HwhaGIjxR&diI!>w$NKxUtZk{=G)(Pb)>F?V z;;PrH9*QUGQhaH5pZgnr;8pLh*A$<6zy&K{rc51X>(|u2Pw&@d7H`vMb(ur{w)L_Z zG5es^zRHYr15b`Ll!bP=PW%L5PH*{>1?JmXZ0tC ztjkHAv|204b_T|YX8oMhDPmeoYSNlZ^;w^%V%D#zgA2-BIXzcioSB*_78Ym9bJBjm y6=zNrXD5Z7DP6`a5`0Cdn&UDeKX&`kBj{cVM|`mn*`NI$!P>!3Zrk(zr}JOm1qxmO delta 2981 zcmYjTU1%KF6+ZXQ%)PU#)vkVKXEu^nl4qoy$cnYv*`1k^%9bKIF*Hp>Nk~gvD2uB| zF8)z$rTHQ43O|(6LgILf+lEk=f?EhBq!>b>tqY+KDWoY)$xEDv_N}F08c6A*`<=UM z@Ll%abIv_K-}zbTzW0THKB;yO6TQFmn#%0!ooiQL+c?wP`{=np5-q(=L^CIjKl}(i zMGE|7@biy9(M%q{^|e0`iLZix>eSip-rwf`@j8(#LH5&~?!~=3cgTYN_i*p-oO$t? zC!)80O=N=&4|cb^-*DcU{1nV5_-4Bh*iGrc)&sw=dv?F~LgAS8CIk%N=`-g~b+4@b zq()@@3jC92yD#j~x8y6h{~Y-FbKSGsmp zEw6{DJER7v6miB>r&*e#kml(SMYKQ-1dZu_IJC(@6E$2FXp)M6SfWuXQ-#JTpmD(Q zs7gN7=stj(rkVbxb)rZ$*Y2B| zp6UP9I_Ur18tZ@AuC;I4pE++Tsjljh&h$6+dv4KPbKfZVh5f=C#Y>|fmmB3*%D*Xp zTp6oesJuJ&*7(MapV^fHYS9)(pe@Y_jzs1Y|hA)q;)S{67N8>?h_ zmUi+ihjN}Jfe-jT?}0BIXT0XvOsZNr)Q??P3gM+ zy6ehj9qhDI8+V*)x$>1{YxByL&8?*3*Mn*`sQZeI?{OWJjH$+gsUedV#u zEutI0^gghV#3C(qP%ld!HY9XQHTG$4bw zPMrF=7XU^(M~pURvcXw&qLc#*K!V-2-^R1M&BW=rO zPY|<_)pLa?>LOgh3wUr2NZW?8uxu7N!jfv>9In{d(YAC{tjFE~V6p(HIK=Uz&n?4>+XlDgy_I5Y{dlbI2JaXUeX%gTsWA1{!D^#eft>3=wXa zWEIbOHGEW5yvIYZ+Q5>!+4*uXaFNm*n^xaX?!vR%|cNab)kaAK;>A&?Ts z#o`cZ&>A+xop7)@3?9%O{IzAC)38hFgNHd?(=#@NLC<&*%3-(08#^@sz#AbSM$2(g zSt)MaI~Ha>o*yW1c!W{_B$Whm4uOVos_g9Uf{xMTh&@&?WoO(x9YZZk0z0md2@iwd z{~MgRYcOHFxF4**lZH;P8Aj_aViTK1@C~1XEV)gN1{R(!9#GVOP-=gP0kW=%3o8z5 zX{~HkGJ{~6hO{?)74ZY++K_he zeEI#5R*-+xKNcQ}kJDj(IoSm~c8b&=(heRq*N3!%{7?Pwh9@TzTBRd+hw;vIi_-po z!l`1idZe{_Bw1~x{fX$&{>dm5Oa1ReE4J|IWdH4GzW-sgZC~8)Ug&2F4~>@2o!>wG z%;{6z{nO{q^`Bpe3htTixt-^_JKO!&7mgn1HwNBm(qJuv!!s6(c?-`nVIIZ&65+Q9 P*84|){ue&)eVqOugycd~ diff --git a/app/assets/fonts/icons.svg b/app/assets/fonts/icons.svg index c107e740d..d92b9a44a 100644 --- a/app/assets/fonts/icons.svg +++ b/app/assets/fonts/icons.svg @@ -19,7 +19,6 @@ - @@ -30,12 +29,10 @@ - - @@ -45,4 +42,8 @@ + + + + diff --git a/app/assets/fonts/icons.ttf b/app/assets/fonts/icons.ttf index b933f1c3f1f5f651f93736c4336dbde93769ad04..f3486cf96bec5abc0d42863006781a3c543eb65b 100644 GIT binary patch delta 1620 zcmaJBU1(cn^nCX}$-TKvlY5gkO>^^iZ_=7HP1~f7P0N_6qs&!1TG^_S#k485O>GOC zAWad$2@Z!B98Pc|J`~);SSf;t3Q`a$8x~(i_mCGKEaICl6Iaho(zV5(9B$6=Ok#B|0yM){xbM42D8aR@>HIzfXF zL?8@N^p?Q046xWQ9#7fu4m-vpVt1WjfD9_MfD7E<0R_C^10C9+-Ta&ymi!&@V4zbA zMZ(co!u*MHx6=&E@q(?@?hqxXA*(Gew@2~%^tN`hmwwxs9F?xBjHE zMXzrJx40_vxDQUuF9jE|ZT*q3K7fCU$E)^bj9iLD!aQez;$b~pH}UXX*w6=ZeY%lT z-CmXV@Pv!#Jw(s-KTY(3Y_16d{Yas&TsGrX6%Vg^Rc|iarxKQR`B=s&R~7cgpJKo6 zSG=Mq@MKbzR~7ZRs&w7&ZP8j@^3jC;YnP!}Vo?K}BsrX-jpw;gC`So`DhO1h!!KV# z3qR{~v8>{h-@D%yw%MFwt4o?&mDPY(QIue8;JJYS4yddzRz%cGsf0?3i4sepl?>G)lIQ5U}5^hx7k+KV^*d zU((BkLLATAM5jY~oZ&;yV9={M7v&cHrFo;t!>a;(OIBLiL42cw=>#`4r}=!SaW{=j?Ug;-Sac$8>t zpSWsWlR0t&`S`Fw-SYGi11kh`MNb$S%r;loTD^~&G>eP(Ws~Mme$TwChm0ZUveuii z;hNgvYLjMhF>f|$4&~pQs&S!r5R&*{Bq0N7#94rRWqvtdS)49unN(Ukrp**9^Gk>a zlfB7Ix;JaC86$);zoLSq%T(hj1}+fQd^WC8#4yY8lDQopGJ}bTIg&V$l;MeGX0bS15*jF3F|Q;7Mgp_JN4)tMSx*#fo%rmYg%4iS{N~?orVW|^ delta 2971 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 IoCHGp4|*~XOaK4? delta 8235 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 X#}?Ogg%yvg%r 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 axJP0`;@$qg%VIBu9*g7nT5vo7?SBBJVA6>I diff --git a/app/assets/stylesheets/icons.scss b/app/assets/stylesheets/icons.scss index eda8b979d..46e24e4cf 100644 --- a/app/assets/stylesheets/icons.scss +++ b/app/assets/stylesheets/icons.scss @@ -38,116 +38,119 @@ } .icon-angle-down:before { - content: "a"; + content: "\61"; } .icon-angle-left:before { - content: "b"; + content: "\62"; } .icon-angle-right:before { - content: "c"; + content: "\63"; } .icon-angle-up:before { - content: "d"; + content: "\64"; } .icon-comments:before { - content: "e"; + content: "\65"; } .icon-twitter:before { - content: "f"; + content: "\66"; } .icon-calendar:before { - content: "g"; + content: "\67"; } .icon-debates:before { - content: "i"; + content: "\69"; } .icon-unlike:before { - content: "j"; + content: "\6a"; } .icon-like:before { - content: "k"; + content: "\6b"; } .icon-check:before { - content: "l"; + content: "\6c"; } .icon-edit:before { - content: "m"; -} -.icon-star:before { - content: "n"; + content: "\6d"; } .icon-user:before { - content: "o"; + content: "\6f"; } .icon-settings:before { - content: "q"; + content: "\71"; } .icon-stats:before { - content: "r"; + content: "\72"; } .icon-proposals:before { - content: "h"; + content: "\68"; } .icon-organizations:before { - content: "s"; + content: "\73"; } .icon-deleted:before { - content: "t"; + content: "\74"; } .icon-tag:before { - content: "u"; + content: "\75"; } .icon-eye:before { - content: "p"; + content: "\70"; } .icon-x:before { - content: "v"; + content: "\76"; } .icon-flag:before { - content: "w"; -} -.icon-notification:before { - content: "x"; + content: "\77"; } .icon-comment:before { - content: "y"; + content: "\79"; } .icon-reply:before { - content: "z"; + content: "\7a"; } .icon-facebook:before { - content: "A"; + content: "\41"; } .icon-google-plus:before { - content: "B"; -} -.icon-language:before { - content: "C"; + content: "\42"; } .icon-search:before { - content: "E"; + content: "\45"; } .icon-external:before { - content: "F"; + content: "\46"; } .icon-video:before { - content: "D"; + content: "\44"; } .icon-document:before { - content: "G"; + content: "\47"; } .icon-print:before { - content: "H"; + content: "\48"; } .icon-blog:before { - content: "J"; + content: "\4a"; } .icon-box:before { - content: "I"; + content: "\49"; } .icon-youtube:before { - content: "K"; + content: "\4b"; } .icon-letter:before { - content: "L"; -} \ No newline at end of file + content: "\4c"; +} +.icon-no-notification:before { + content: "\78"; +} +.icon-notification:before { + content: "\6e"; +} +.icon-circle:before { + content: "\43"; +} +.icon-circle-o:before { + content: "\4d"; +} From e008d1f642cea4e99ac701563ebc3adf9c339061 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Cabeza Date: Fri, 8 Jan 2016 12:45:37 +0100 Subject: [PATCH 11/64] Adds new styles for notifications --- app/assets/stylesheets/layout.scss | 73 ++++++++++++++++++- app/assets/stylesheets/variables.scss | 1 + app/views/devise/menu/_login_items.html.erb | 14 +++- .../notifications/_notification.html.erb | 10 ++- app/views/notifications/index.html.erb | 28 ++++--- config/locales/en.yml | 4 +- config/locales/es.yml | 4 +- 7 files changed, 112 insertions(+), 22 deletions(-) diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index d47189d18..329ab5a0e 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -384,7 +384,6 @@ header { &:hover { background: none; color: white; - text-decoration: underline; transition: text-decoration 275ms; } @@ -408,6 +407,7 @@ header { &:hover, &:focus { background-color: #007095 !important; + text-decoration: underline; } } @@ -935,6 +935,77 @@ img.avatar, img.admin-avatar, img.moderator-avatar, img.initialjs-avatar { } } +.notifications { + position: relative; + + &:hover { + text-decoration: none; + } + + [class^="icon-"] { + font-size: $h4-font-size; + vertical-align: middle; + } + + .icon-circle { + color: #ecf00b; + font-size: $tiny-font-size; + position: absolute; + right: 4px; + top: -6px; + } +} + +.notifications-list:before { + background: $border; + content: ''; + height: 100%; + left: 28px; + position: absolute; + top: 0; + width: 2px; +} + +.notification { + display: block; + padding: $line-height/2 0 $line-height/2 $line-height*1.5; + position: relative; + + &:hover { + + a { + text-decoration: none; + } + + p:not(.time) { + color: $link; + } + + &:before { + content: "\43"; + } + } + + &:before { + background: white; + color: $brand; + content: "\4d"; + font-family: "icons" !important; + left: 6px; + position: absolute; + } + + p { + color: $text; + margin-bottom: 0; + } + + .time { + font-size: $small-font-size; + color: $text-medium; + } +} + // 09. Filters & search // - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/assets/stylesheets/variables.scss b/app/assets/stylesheets/variables.scss index 72fb27788..336b09919 100644 --- a/app/assets/stylesheets/variables.scss +++ b/app/assets/stylesheets/variables.scss @@ -39,6 +39,7 @@ $h6-font-size: rem-calc(13); $h6-line-height: rem-calc(17); $small-font-size: rem-calc(14); +$tiny-font-size: rem-calc(10); $line-height: rem-calc(24); // 02. Colors diff --git a/app/views/devise/menu/_login_items.html.erb b/app/views/devise/menu/_login_items.html.erb index 5f6608f2f..d2460d11d 100644 --- a/app/views/devise/menu/_login_items.html.erb +++ b/app/views/devise/menu/_login_items.html.erb @@ -1,5 +1,16 @@
      <% if user_signed_in? %> +
    • + <%= link_to notifications_path, class: "notifications" do %> + <% if current_user.notifications.count > 0 %> + + + + <% else %> + + <% end %> + <% end %> +
    • <%= link_to(t("layouts.header.my_activity_link"), user_path(current_user)) %>
    • @@ -9,9 +20,6 @@
    • <%= link_to(t("devise_views.menu.login_items.logout"), destroy_user_session_path, method: :delete) %>
    • -
    • - <%= link_to 'Notificaciones', notifications_path, class: notifications_class_for(current_user) %> -
    • <% else %>
    • <%= link_to(t("devise_views.menu.login_items.login"), new_user_session_path) %> diff --git a/app/views/notifications/_notification.html.erb b/app/views/notifications/_notification.html.erb index 313a5458c..7315f899d 100644 --- a/app/views/notifications/_notification.html.erb +++ b/app/views/notifications/_notification.html.erb @@ -1,8 +1,10 @@
    • <%= link_to notification do %> -  •  - <%= notification.username %> - <%= t("notifications.index.#{notification_action(notification)}") %> - <%= notification.notifiable.commentable.title %> +

      + <%= notification.username %> + <%= t("notifications.index.#{notification_action(notification)}") %> + <%= notification.notifiable.commentable.title %> +

      +

      <%= l notification.timestamp, format: :datetime %>

      <% end %>
    • \ No newline at end of file diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index 5eaebefb9..cc9170e48 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -1,14 +1,18 @@ -<% if @notifications.empty? %> -
      <%= t("notifications.index.empty_notifications") %>
      -<% else %> -
      -
        - <%= render @notifications %> -
      +
      +
      + <% if @notifications.empty? %> +
      + <%= t("notifications.index.empty_notifications") %> +
      + <% else %> +
      + <%= link_to t("notifications.index.mark_all_as_read"), + mark_all_as_read_notifications_path, method: :put %> +
      -
      - <%= link_to t("notifications.index.mark_all_as_read"), - mark_all_as_read_notifications_path, method: :put %> -
      +
        + <%= render @notifications %> +
      + <% end %>
      -<% end %> \ No newline at end of file +
      diff --git a/config/locales/en.yml b/config/locales/en.yml index 683db0454..d46c6f5ab 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -29,6 +29,8 @@ en: more_information: "More information" debates: "Debates" proposals: "Proposals" + new_notifications: "You have %{count} new notifications" + no_notifications: "You don't have new notifications" footer: description: "This portal uses the %{consul} which is %{open_source}. From Madrid out into the world." open_source: "open-source software" @@ -313,7 +315,7 @@ en: notifications: index: mark_all_as_read: "Mark all as read" - empty_notifications: "There are no new notifications." + empty_notifications: "You don't have new notifications." commented_on_your_debate: "commented on your debate" replied_to_your_comment: "replied to your comment on" simple_captcha: diff --git a/config/locales/es.yml b/config/locales/es.yml index f6fd907de..531456d8f 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -29,6 +29,8 @@ es: more_information: "Más información" debates: "Debates" proposals: "Propuestas" + new_notifications: "Tienes %{count} notificaciones nuevas" + no_notifications: "No tienes notificaciones nuevas" footer: description: "Este portal usa la %{consul} que es %{open_source}. De Madrid, para el mundo entero." open_source: "software libre" @@ -313,7 +315,7 @@ es: notifications: index: mark_all_as_read: "Marcar todas como leídas" - empty_notifications: "No hay notificaciones nuevas." + empty_notifications: "No tienes notificaciones nuevas." commented_on_your_debate: "ha comentado en tu debate" replied_to_your_comment: "ha respondido a tu comentario en" simple_captcha: From 40ffeeaeb01c41ec1c876621bbf8652836e5196a Mon Sep 17 00:00:00 2001 From: Alberto Garcia Cabeza Date: Fri, 8 Jan 2016 12:46:02 +0100 Subject: [PATCH 12/64] Removes unused method --- app/helpers/notifications_helper.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 1945d7c5b..63618440b 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -3,9 +3,4 @@ module NotificationsHelper def notification_action(notification) notification.notifiable.reply? ? "replied_to_your_comment" : "commented_on_your_debate" end - - def notifications_class_for(user) - user.notifications.count > 0 ? "with_notifications" : "without_notifications" - end - end From ab74bd58faefb6e473724f1f73b27e99783de46b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baza=CC=81n?= Date: Fri, 8 Jan 2016 12:35:23 +0100 Subject: [PATCH 13/64] avoids queries when creating a notification --- app/controllers/comments_controller.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index c30ddbaf2..2ce536c5c 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -65,11 +65,11 @@ class CommentsController < ApplicationController def add_notification(comment) if comment.reply? - author = comment.parent.author + notifiable = comment.parent else - author = comment.commentable.author + notifiable = comment.commentable end - author.notifications.create!(notifiable: comment) unless comment.made_by? author + Notification.create!(user_id: notifiable.author_id, notifiable: comment) unless comment.author_id == notifiable.author_id end end From 790d8de5b6335f787ea9bcf533433431b61e7dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Fri, 8 Jan 2016 12:35:56 +0100 Subject: [PATCH 14/64] removes unused method --- app/models/comment.rb | 4 ---- spec/models/comment_spec.rb | 14 -------------- 2 files changed, 18 deletions(-) diff --git a/app/models/comment.rb b/app/models/comment.rb index 44e16b729..3771af84a 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -90,10 +90,6 @@ class Comment < ActiveRecord::Base !root? end - def made_by?(user) - self.user == user - end - def call_after_commented self.commentable.try(:after_commented) end diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index 684de6438..73b70fb42 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -129,18 +129,4 @@ describe Comment do end end - describe "#made_by?" do - let(:author) { create :user } - let(:comment) { create :comment, user: author } - - it "returns true if comment was made by user" do - expect(comment.made_by?(author)).to be true - end - - it "returns false if comment was not made by user" do - not_author = create :user - expect(comment.made_by?(not_author)).to be false - end - end - end From a976e16ef345acd744410fbeff9bc1b03f7c021b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Fri, 8 Jan 2016 12:36:43 +0100 Subject: [PATCH 15/64] redirects notification show to notifiable --- app/controllers/notifications_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 3820d7e36..a4ec31b50 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -9,7 +9,7 @@ class NotificationsController < ApplicationController def show @notification = current_user.notifications.find(params[:id]) - redirect_to url_for(@notification.notifiable.commentable) + redirect_to url_for(@notification.notifiable) end def mark_all_as_read From 5ef9f4de873cf6d507f58e30e716495e86d2523a Mon Sep 17 00:00:00 2001 From: Alberto Garcia Cabeza Date: Fri, 8 Jan 2016 13:41:18 +0100 Subject: [PATCH 16/64] Improves styles for comments show --- app/assets/stylesheets/layout.scss | 10 +++------- app/views/comments/_comment.html.erb | 8 ++++---- app/views/comments/show.html.erb | 12 ++++++++---- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index d47189d18..ffb2c6c76 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -1582,7 +1582,6 @@ table { font-family: $font-sans; font-size: $small-font-size; line-height: $line-height; - margin: rem-calc(10) $line-height/2 $line-height/4 0; a { color: $text-light; @@ -1603,15 +1602,12 @@ table { .comment-body { margin-left: rem-calc(42); - p { - font-size: $small-font-size; - } - .reply { background: white; border: 1px solid $border; - font-family: $font-sans; - font-size: rem-calc(12); + border-left: 0; + border-right: 0; + font-size: $small-font-size; margin: rem-calc(6) 0; padding: rem-calc(6); diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index 44e15362b..c001da74e 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -67,11 +67,11 @@ <%= simple_format text_with_links comment.body %>
      - - <%= render 'comments/votes', comment: comment %> - -
      + + <%= render 'comments/votes', comment: comment %> + + <%= t("comments.comment.responses", count: comment.children.size) %> <% if user_signed_in? %> diff --git a/app/views/comments/show.html.erb b/app/views/comments/show.html.erb index b0c3dfb3f..4fcbdead4 100644 --- a/app/views/comments/show.html.erb +++ b/app/views/comments/show.html.erb @@ -1,9 +1,13 @@ -
      - <%= link_to t("comments.show.return_to_commentable") + @comment.commentable.title, - @comment.commentable %> +
      +
      + <%= link_to @comment.commentable, class: "left back" do %> + + <%= t("comments.show.return_to_commentable") + @comment.commentable.title %> + <% end %> +
      -
      +
      <%= render @comment %> From 704a038795d2c9714ac0f6c6aaec308f24578555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Fri, 8 Jan 2016 14:32:16 +0100 Subject: [PATCH 17/64] groups notifications --- app/controllers/comments_controller.rb | 2 +- app/helpers/notifications_helper.rb | 2 +- app/models/notification.rb | 14 ++-- app/views/comments/_comment.html.erb | 2 +- .../notifications/_notification.html.erb | 5 +- config/locales/en.yml | 8 +- config/locales/es.yml | 8 +- ...0108114750_add_counter_to_notifications.rb | 5 ++ db/schema.rb | 3 +- spec/factories.rb | 2 +- spec/features/notifications_spec.rb | 84 ++++++++++++++++--- spec/helpers/notifications_helper_spec.rb | 25 +----- spec/models/notification_spec.rb | 10 +-- 13 files changed, 111 insertions(+), 59 deletions(-) create mode 100644 db/migrate/20160108114750_add_counter_to_notifications.rb diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 2ce536c5c..b728ba729 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -69,7 +69,7 @@ class CommentsController < ApplicationController else notifiable = comment.commentable end - Notification.create!(user_id: notifiable.author_id, notifiable: comment) unless comment.author_id == notifiable.author_id + Notification.add(notifiable.author_id, notifiable) unless comment.author_id == notifiable.author_id end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 63618440b..281163380 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,6 +1,6 @@ module NotificationsHelper def notification_action(notification) - notification.notifiable.reply? ? "replied_to_your_comment" : "commented_on_your_debate" + notification.notifiable_type == "Comment" ? "replies_to" : "comments_on" end end diff --git a/app/models/notification.rb b/app/models/notification.rb index e22e68ad2..7d3ebd146 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -4,11 +4,7 @@ class Notification < ActiveRecord::Base scope :unread, -> { all } scope :recent, -> { order(id: :desc) } - scope :for_render, -> { includes(notifiable: [:user]) } - - def username - notifiable.user.username - end + scope :for_render, -> { includes(:notifiable) } def timestamp notifiable.created_at @@ -17,4 +13,12 @@ class Notification < ActiveRecord::Base def mark_as_read self.destroy end + + def self.add(user_id, notifiable) + if notification = Notification.find_by(user_id: user_id, notifiable: notifiable) + Notification.increment_counter(:counter, notification.id) + else + Notification.create!(user_id: user_id, notifiable: notifiable) + end + end end \ No newline at end of file diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index 9178f0b9b..00d62ed5f 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -71,7 +71,7 @@ <%= render 'comments/votes', comment: comment %> -
      +
      <%= t("comments.comment.responses", count: child_comments_of(comment).size) %> <% if user_signed_in? %> diff --git a/app/views/notifications/_notification.html.erb b/app/views/notifications/_notification.html.erb index 7315f899d..28def42f7 100644 --- a/app/views/notifications/_notification.html.erb +++ b/app/views/notifications/_notification.html.erb @@ -1,9 +1,8 @@
    • <%= link_to notification do %>

      - <%= notification.username %> - <%= t("notifications.index.#{notification_action(notification)}") %> - <%= notification.notifiable.commentable.title %> + <%= t("notifications.index.#{notification_action(notification)}", count: notification.counter) %> + <%= notification.notifiable.is_a?(Comment) ? notification.notifiable.commentable.title : notification.notifiable.title %>

      <%= l notification.timestamp, format: :datetime %>

      <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index e8d82bcf8..d04cba8fe 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -313,8 +313,12 @@ en: index: mark_all_as_read: "Mark all as read" empty_notifications: "You don't have new notifications." - commented_on_your_debate: "commented on your debate" - replied_to_your_comment: "replied to your comment on" + comments_on: + one: "Someone commented on" + other: "There are %{count} new comments on" + replies_to: + one: "Someone replied to your comment on" + other: "There are %{count} new replies to your comment on" simple_captcha: placeholder: "Enter the text from the image" label: "Enter the text from the image in the box below" diff --git a/config/locales/es.yml b/config/locales/es.yml index 6d6f9a72b..bef5af656 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -313,8 +313,12 @@ es: index: mark_all_as_read: "Marcar todas como leídas" empty_notifications: "No tienes notificaciones nuevas." - commented_on_your_debate: "ha comentado en tu debate" - replied_to_your_comment: "ha respondido a tu comentario en" + comments_on: + one: "Hay un nuevo comentario en" + other: "Hay %{count} comentarios nuevos en" + replies_to: + one: "Hay una respuesta nueva a tu comentario en" + other: "Hay %{count} nuevas respuestas a tu comentario en" simple_captcha: placeholder: "Introduce el texto de la imagen" label: "Introduce el texto de la imagen en la siguiente caja" diff --git a/db/migrate/20160108114750_add_counter_to_notifications.rb b/db/migrate/20160108114750_add_counter_to_notifications.rb new file mode 100644 index 000000000..7cf3c434c --- /dev/null +++ b/db/migrate/20160108114750_add_counter_to_notifications.rb @@ -0,0 +1,5 @@ +class AddCounterToNotifications < ActiveRecord::Migration + def change + add_column :notifications, :counter, :integer, default: 1 + end +end diff --git a/db/schema.rb b/db/schema.rb index 35aa4b68c..1ba98a0a3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160105170113) do +ActiveRecord::Schema.define(version: 20160108114750) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -208,6 +208,7 @@ ActiveRecord::Schema.define(version: 20160105170113) do t.integer "user_id" t.integer "notifiable_id" t.string "notifiable_type" + t.integer "counter", default: 1 end add_index "notifications", ["user_id"], name: "index_notifications_on_user_id", using: :btree diff --git a/spec/factories.rb b/spec/factories.rb index d48aba942..6e910a214 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -292,7 +292,7 @@ FactoryGirl.define do factory :notification do user - association :notifiable, factory: :comment + association :notifiable, factory: :proposal end end diff --git a/spec/features/notifications_spec.rb b/spec/features/notifications_spec.rb index 22e4b320d..1ffc6a854 100644 --- a/spec/features/notifications_spec.rb +++ b/spec/features/notifications_spec.rb @@ -4,6 +4,7 @@ feature "Notifications" do let(:author) { create :user } let(:user) { create :user } let(:debate) { create :debate, author: author } + let(:proposal) { create :proposal, author: author } scenario "User commented on my debate", :js do login_as user @@ -18,12 +19,44 @@ feature "Notifications" do logout login_as author visit root_path - expect(page).to have_xpath "//a[@class='with_notifications' and @href='#{notifications_path}' and text()='Notificaciones']" - click_link "Notificaciones" + find(".icon-notification").click + expect(page).to have_css ".notification", count: 1 - expect(page).to have_content user.username - expect(page).to have_content "commented on your debate" + + expect(page).to have_content "Someone commented on" + expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" + end + + scenario "Multiple comments on my proposal", :js do + login_as user + visit proposal_path proposal + + fill_in "comment-body-proposal_#{proposal.id}", with: "I agree" + click_button "Publish comment" + within "#comments" do + expect(page).to have_content "I agree" + end + + logout + login_as create(:user) + visit proposal_path proposal + + fill_in "comment-body-proposal_#{proposal.id}", with: "I disagree" + click_button "Publish comment" + within "#comments" do + expect(page).to have_content "I disagree" + end + + logout + login_as author + visit root_path + + find(".icon-notification").click + + expect(page).to have_css ".notification", count: 1 + + expect(page).to have_content "There are 2 new comments on" expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" end @@ -45,12 +78,39 @@ feature "Notifications" do logout login_as author visit root_path - expect(page).to have_xpath "//a[@class='with_notifications' and @href='#{notifications_path}' and text()='Notificaciones']" - visit notifications_path + find(".icon-notification").click + expect(page).to have_css ".notification", count: 1 - expect(page).to have_content user.username - expect(page).to have_content "replied to your comment on" + expect(page).to have_content "Someone replied to your comment on" + expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" + end + + scenario "Multiple replies to my comment", :js do + comment = create :comment, commentable: debate, user: author + 3.times do |n| + login_as create(:user) + visit debate_path debate + + within("#comment_#{comment.id}_reply") { click_link "Reply" } + within "#js-comment-form-comment_#{comment.id}" do + fill_in "comment-body-comment_#{comment.id}", with: "Reply number #{n}" + click_button "Publish reply" + end + + within "#comment_#{comment.id}" do + expect(page).to have_content "Reply number #{n}" + end + logout + end + + login_as author + visit root_path + + find(".icon-notification").click + + expect(page).to have_css ".notification", count: 1 + expect(page).to have_content "There are 3 new replies to your comment on" expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" end @@ -63,9 +123,8 @@ feature "Notifications" do within "#comments" do expect(page).to have_content "I commented on my own debate" end - expect(page).to have_xpath "//a[@class='without_notifications' and @href='#{notifications_path}' and text()='Notificaciones']" - click_link "Notificaciones" + find(".icon-no-notification").click expect(page).to have_css ".notification", count: 0 end @@ -83,7 +142,8 @@ feature "Notifications" do within "#comment_#{comment.id}" do expect(page).to have_content "I replied to my own comment" end - expect(page).to have_xpath "//a[@class='without_notifications' and @href='#{notifications_path}' and text()='Notificaciones']" + + find(".icon-no-notification") visit notifications_path expect(page).to have_css ".notification", count: 0 @@ -126,7 +186,7 @@ feature "Notifications" do login_as user visit notifications_path - expect(page).to have_content "There are no new notifications" + expect(page).to have_content "You don't have new notifications" end end diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb index fa378b2ec..ccb3b3a9d 100644 --- a/spec/helpers/notifications_helper_spec.rb +++ b/spec/helpers/notifications_helper_spec.rb @@ -5,38 +5,21 @@ describe NotificationsHelper do describe "#notification_action" do let(:debate) { create :debate } let(:debate_comment) { create :comment, commentable: debate } - let(:comment_reply) { create :comment, commentable: debate, parent: debate_comment } context "when action was comment on a debate" do it "returns correct text when someone comments on your debate" do - notification = create :notification, notifiable: debate_comment - expect(notification_action(notification)).to eq "commented_on_your_debate" + notification = create :notification, notifiable: debate + expect(notification_action(notification)).to eq "comments_on" end end context "when action was comment on a debate" do it "returns correct text when someone replies to your comment" do - notification = create :notification, notifiable: comment_reply - expect(notification_action(notification)).to eq "replied_to_your_comment" + notification = create :notification, notifiable: debate_comment + expect(notification_action(notification)).to eq "replies_to" end end end - describe "#notifications_class_for" do - let(:user) { create :user } - - context "when user doesn't have notifications" do - it "returns class 'without_notifications'" do - expect(notifications_class_for(user)).to eq "without_notifications" - end - end - - context "when user has notifications" do - it "returns class 'with_notifications'" do - notification = create :notification, user: user - expect(notifications_class_for(user)).to eq "with_notifications" - end - end - end end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index 0b8354e83..361211a51 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -23,7 +23,7 @@ describe Notification do describe "#for_render (scope)" do it "returns notifications including notifiable and user" do - expect(Notification).to receive(:includes).with(notifiable: [:user]).exactly(:once) + expect(Notification).to receive(:includes).with(:notifiable).exactly(:once) Notification.for_render end end @@ -47,12 +47,4 @@ describe Notification do end end - describe "#username" do - it "returns the username of the activity's author" do - comment = create :comment - notification = create :notification, notifiable: comment - expect(notification.username).to eq comment.author.username - end - end - end From 432e9e0d5bf0e64244c0d6d1c6145bc9bb98b84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Fri, 8 Jan 2016 14:39:34 +0100 Subject: [PATCH 18/64] adds counter cache for user's notifications --- app/models/notification.rb | 2 +- app/views/devise/menu/_login_items.html.erb | 4 ++-- ...20160108133501_add_notifications_counter_cache_to_user.rb | 5 +++++ db/schema.rb | 3 ++- 4 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20160108133501_add_notifications_counter_cache_to_user.rb diff --git a/app/models/notification.rb b/app/models/notification.rb index 7d3ebd146..1cb500ccf 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -1,5 +1,5 @@ class Notification < ActiveRecord::Base - belongs_to :user + belongs_to :user, counter_cache: true belongs_to :notifiable, polymorphic: true scope :unread, -> { all } diff --git a/app/views/devise/menu/_login_items.html.erb b/app/views/devise/menu/_login_items.html.erb index d2460d11d..7681619e1 100644 --- a/app/views/devise/menu/_login_items.html.erb +++ b/app/views/devise/menu/_login_items.html.erb @@ -2,9 +2,9 @@ <% if user_signed_in? %>
    • <%= link_to notifications_path, class: "notifications" do %> - <% if current_user.notifications.count > 0 %> + <% if current_user.notifications_count > 0 %> - + <% else %> diff --git a/db/migrate/20160108133501_add_notifications_counter_cache_to_user.rb b/db/migrate/20160108133501_add_notifications_counter_cache_to_user.rb new file mode 100644 index 000000000..e106df6ae --- /dev/null +++ b/db/migrate/20160108133501_add_notifications_counter_cache_to_user.rb @@ -0,0 +1,5 @@ +class AddNotificationsCounterCacheToUser < ActiveRecord::Migration + def change + add_column :users, :notifications_count, :integer, default: 0 + end +end diff --git a/db/schema.rb b/db/schema.rb index 1ba98a0a3..162bd816e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160108114750) do +ActiveRecord::Schema.define(version: 20160108133501) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -339,6 +339,7 @@ ActiveRecord::Schema.define(version: 20160108114750) do t.datetime "erased_at" t.boolean "public_activity", default: true t.boolean "newsletter", default: false + t.integer "notifications_count", default: 0 end add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree From 1c801a73989d77ad31cc85a2f6aaac234128ce91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Fri, 8 Jan 2016 14:44:22 +0100 Subject: [PATCH 19/64] adds ignore_unused i18n keys --- config/i18n-tasks.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 081619394..6b815cd31 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -127,8 +127,8 @@ ignore_unused: - 'proposals.index.select_order' - 'proposals.index.orders.*' - 'proposals.index.search_form.*' - - 'notifications.index.commented_on_your_debate' - - 'notifications.index.replied_to_your_comment' + - 'notifications.index.comments_on*' + - 'notifications.index.replies_to*' - 'helpers.page_entries_info.*' # kaminari - 'views.pagination.*' # kaminari # - '{devise,kaminari,will_paginate}.*' From fb7550c7de93528793f7006574aafb0bb1b8cf6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Fri, 8 Jan 2016 15:12:10 +0100 Subject: [PATCH 20/64] fixes build --- app/views/comments/_comment.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index c001da74e..e8c49636b 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -67,7 +67,7 @@ <%= simple_format text_with_links comment.body %>
    • -
      +
      <%= render 'comments/votes', comment: comment %> From c8ad7a8fb2d1624efa9b25062a9a0c2b40cd03b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Fri, 8 Jan 2016 17:28:27 +0100 Subject: [PATCH 21/64] pluralizes notifications alert --- config/locales/en.yml | 4 +++- config/locales/es.yml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index 97494fd81..c40575d6b 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -29,7 +29,9 @@ en: more_information: "More information" debates: "Debates" proposals: "Proposals" - new_notifications: "You have %{count} new notifications" + new_notifications: + one: "You have a new notification" + other: "You have %{count} new notifications" no_notifications: "You don't have new notifications" footer: description: "This portal uses the %{consul} which is %{open_source}. From Madrid out into the world." diff --git a/config/locales/es.yml b/config/locales/es.yml index 7fb02c729..37be2e03a 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -29,7 +29,9 @@ es: more_information: "Más información" debates: "Debates" proposals: "Propuestas" - new_notifications: "Tienes %{count} notificaciones nuevas" + new_notifications: + one: "Tienes una nueva notificación" + other: "Tienes %{count} notificaciones nuevas" no_notifications: "No tienes notificaciones nuevas" footer: description: "Este portal usa la %{consul} que es %{open_source}. De Madrid, para el mundo entero." From a6f8c79268490883e06d294373afe11839863d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Fri, 18 Dec 2015 12:55:19 +0100 Subject: [PATCH 22/64] adds SpendingProposal model --- app/models/spending_proposal.rb | 16 +++++++ ...0151218114205_create_spending_proposals.rb | 12 +++++ db/schema.rb | 11 ++++- spec/factories.rb | 8 ++++ spec/models/spending_proposal_spec.rb | 44 +++++++++++++++++++ 5 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 app/models/spending_proposal.rb create mode 100644 db/migrate/20151218114205_create_spending_proposals.rb create mode 100644 spec/models/spending_proposal_spec.rb diff --git a/app/models/spending_proposal.rb b/app/models/spending_proposal.rb new file mode 100644 index 000000000..12fa816df --- /dev/null +++ b/app/models/spending_proposal.rb @@ -0,0 +1,16 @@ +class SpendingProposal < ActiveRecord::Base + include Measurable + include Sanitizable + include Taggable + + belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + + validates :title, presence: true + validates :author, presence: true + validates :description, presence: true + + validates :title, length: { in: 4..Proposal.title_max_length } + validates :description, length: { maximum: Proposal.description_max_length } + + validates :terms_of_service, acceptance: { allow_nil: false }, on: :create +end diff --git a/db/migrate/20151218114205_create_spending_proposals.rb b/db/migrate/20151218114205_create_spending_proposals.rb new file mode 100644 index 000000000..5bcd3aba1 --- /dev/null +++ b/db/migrate/20151218114205_create_spending_proposals.rb @@ -0,0 +1,12 @@ +class CreateSpendingProposals < ActiveRecord::Migration + def change + create_table :spending_proposals do |t| + t.string :title + t.text :description + t.integer :author_id + t.string :external_url + + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 162bd816e..910fc5c54 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -191,7 +191,7 @@ ActiveRecord::Schema.define(version: 20160108133501) do create_table "locks", force: :cascade do |t| t.integer "user_id" t.integer "tries", default: 0 - t.datetime "locked_until", default: '2000-01-01 07:01:01', null: false + t.datetime "locked_until", default: '2000-01-01 00:01:01', null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end @@ -273,6 +273,15 @@ ActiveRecord::Schema.define(version: 20160108133501) do add_index "simple_captcha_data", ["key"], name: "idx_key", using: :btree + create_table "spending_proposals", force: :cascade do |t| + t.string "title" + t.text "description" + t.integer "author_id" + t.string "external_url" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "taggings", force: :cascade do |t| t.integer "tag_id" t.integer "taggable_id" diff --git a/spec/factories.rb b/spec/factories.rb index 6e910a214..0d1aded2a 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -180,6 +180,14 @@ FactoryGirl.define do end end + factory :spending_proposal do + sequence(:title) { |n| "Spending Proposal #{n} title" } + description 'Spend money on this' + external_url 'http://external_documention.org' + terms_of_service '1' + association :author, factory: :user + end + factory :vote do association :votable, factory: :debate association :voter, factory: :user diff --git a/spec/models/spending_proposal_spec.rb b/spec/models/spending_proposal_spec.rb new file mode 100644 index 000000000..e9b5f8ee4 --- /dev/null +++ b/spec/models/spending_proposal_spec.rb @@ -0,0 +1,44 @@ +require 'rails_helper' + +describe SpendingProposal do + let(:spending_proposal) { build(:spending_proposal) } + + it "should be valid" do + expect(spending_proposal).to be_valid + end + + it "should not be valid without an author" do + spending_proposal.author = nil + expect(spending_proposal).to_not be_valid + end + + describe "#title" do + it "should not be valid without a title" do + spending_proposal.title = nil + expect(spending_proposal).to_not be_valid + end + + it "should not be valid when very short" do + spending_proposal.title = "abc" + expect(spending_proposal).to_not be_valid + end + + it "should not be valid when very long" do + spending_proposal.title = "a" * 81 + expect(spending_proposal).to_not be_valid + end + end + + describe "#description" do + it "should be sanitized" do + spending_proposal.description = "" + spending_proposal.valid? + expect(spending_proposal.description).to eq("alert('danger');") + end + + it "should not be valid when very long" do + spending_proposal.description = "a" * 6001 + expect(spending_proposal).to_not be_valid + end + end +end From fe32162c9f4a1ebb8d52e62bbfdb16dbbca75da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Fri, 25 Dec 2015 21:16:20 +0100 Subject: [PATCH 23/64] ups redcarpet --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index f0f5a9dbe..241d301df 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -310,7 +310,7 @@ GEM thor (>= 0.18.1, < 2.0) raindrops (0.15.0) rake (10.4.2) - redcarpet (3.3.3) + redcarpet (3.3.4) referer-parser (0.3.0) request_store (1.2.1) responders (2.1.1) From 2c36ceb10c1c2932d6ba79f03a115c94f7f09415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baza=CC=81n?= Date: Wed, 30 Dec 2015 14:18:44 +0100 Subject: [PATCH 24/64] adds abilities for SpendingProposals --- app/controllers/moderation/users_controller.rb | 2 +- app/models/abilities/common.rb | 3 +++ app/models/abilities/everyone.rb | 1 + app/views/proposals/_form.html.erb | 4 +--- spec/models/abilities/common_spec.rb | 3 +++ spec/models/abilities/everyone_spec.rb | 3 +++ 6 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/controllers/moderation/users_controller.rb b/app/controllers/moderation/users_controller.rb index 6ff22f94a..3255bb6cc 100644 --- a/app/controllers/moderation/users_controller.rb +++ b/app/controllers/moderation/users_controller.rb @@ -30,4 +30,4 @@ class Moderation::UsersController < Moderation::BaseController Activity.log(current_user, :block, @user) end -end +end \ No newline at end of file diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb index 181238d2c..b95e5f5b4 100644 --- a/app/models/abilities/common.rb +++ b/app/models/abilities/common.rb @@ -17,9 +17,12 @@ module Abilities proposal.editable_by?(user) end + can :read, SpendingProposal + can :create, Comment can :create, Debate can :create, Proposal + can :create, SpendingProposal can [:flag, :unflag], Comment cannot [:flag, :unflag], Comment, user_id: user.id diff --git a/app/models/abilities/everyone.rb b/app/models/abilities/everyone.rb index 122d5db2a..377ef7440 100644 --- a/app/models/abilities/everyone.rb +++ b/app/models/abilities/everyone.rb @@ -6,6 +6,7 @@ module Abilities can :read, Debate can :read, Proposal can :read, Comment + can :read, SpendingProposal can :read, Legislation can :read, User can [:search, :read], Annotation diff --git a/app/views/proposals/_form.html.erb b/app/views/proposals/_form.html.erb index 4c14730fe..97e1ecb6b 100644 --- a/app/views/proposals/_form.html.erb +++ b/app/views/proposals/_form.html.erb @@ -79,6 +79,4 @@ <%= f.submit(class: "button radius", value: t("proposals.#{action_name}.form.submit_button")) %>
      -<% end %> - - +<% end %> \ No newline at end of file diff --git a/spec/models/abilities/common_spec.rb b/spec/models/abilities/common_spec.rb index 4775cd1ee..a24b5f2d9 100644 --- a/spec/models/abilities/common_spec.rb +++ b/spec/models/abilities/common_spec.rb @@ -28,6 +28,9 @@ describe "Abilities::Common" do it { should_not be_able_to(:vote, Proposal) } it { should_not be_able_to(:vote_featured, Proposal) } + it { should be_able_to(:index, SpendingProposal) } + it { should be_able_to(:create, SpendingProposal) } + it { should_not be_able_to(:comment_as_administrator, debate) } it { should_not be_able_to(:comment_as_moderator, debate) } it { should_not be_able_to(:comment_as_administrator, proposal) } diff --git a/spec/models/abilities/everyone_spec.rb b/spec/models/abilities/everyone_spec.rb index dacac25e4..3f1e57278 100644 --- a/spec/models/abilities/everyone_spec.rb +++ b/spec/models/abilities/everyone_spec.rb @@ -23,4 +23,7 @@ describe "Abilities::Everyone" do it { should_not be_able_to(:unflag, Proposal) } it { should be_able_to(:show, Comment) } + + it { should be_able_to(:index, SpendingProposal) } + it { should_not be_able_to(:create, SpendingProposal) } end From 864ef6e73b9214b33db370111d7d00b5131890ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Mon, 4 Jan 2016 16:56:58 +0100 Subject: [PATCH 25/64] adds basic spending proposals index --- app/controllers/spending_proposals_controller.rb | 7 +++++++ app/views/layouts/_header.html.erb | 1 + app/views/spending_proposals/index.html.erb | 13 +++++++++++++ config/locales/en.yml | 7 +++++++ config/locales/es.yml | 7 +++++++ config/routes.rb | 2 ++ spec/features/spending_proposals_spec.rb | 11 +++++++++++ 7 files changed, 48 insertions(+) create mode 100644 app/controllers/spending_proposals_controller.rb create mode 100644 app/views/spending_proposals/index.html.erb create mode 100644 spec/features/spending_proposals_spec.rb diff --git a/app/controllers/spending_proposals_controller.rb b/app/controllers/spending_proposals_controller.rb new file mode 100644 index 000000000..1abc71549 --- /dev/null +++ b/app/controllers/spending_proposals_controller.rb @@ -0,0 +1,7 @@ +class SpendingProposalsController < ApplicationController + before_action :authenticate_user!, except: [:index] + + def index + end + +end \ No newline at end of file diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb index 78899aba5..373871aff 100644 --- a/app/views/layouts/_header.html.erb +++ b/app/views/layouts/_header.html.erb @@ -36,6 +36,7 @@
      <%= link_to t("layouts.header.debates"), debates_path, class: ("active" if current_page?(controller: "/debates")) %> <%= link_to t("layouts.header.proposals"), proposals_path, class: ("active" if current_page?(controller: "/proposals")) %> + <%= link_to t("layouts.header.spending_proposals"), spending_proposals_path, class: ("active" if current_page?(controller: "/spending_proposals")) %> <%= link_to t("layouts.header.more_information"), page_path('more_information'), class: ("active" if current_page?("/more_information")) %> <%= link_to t("layouts.header.external_link_blog_url"), target: "_blank" do %> <%= t("layouts.header.external_link_blog") %> diff --git a/app/views/spending_proposals/index.html.erb b/app/views/spending_proposals/index.html.erb new file mode 100644 index 000000000..3d264bc92 --- /dev/null +++ b/app/views/spending_proposals/index.html.erb @@ -0,0 +1,13 @@ +<% provide :title do %><%= t('spending_proposals.index.title') %><% end %> +
      +
      +
      +

      <%= t('spending_proposals.index.title') %>

      + +

      <%= t('spending_proposals.index.text') %>

      + + <%= link_to t('spending_proposals.index.create_link'), new_spending_proposal_path, class: 'button radius' %> + +
      +
      +
      \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index c40575d6b..40d9d4973 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -29,6 +29,7 @@ en: more_information: "More information" debates: "Debates" proposals: "Proposals" + spending_proposals: "Spending proposals" new_notifications: one: "You have a new notification" other: "You have %{count} new notifications" @@ -75,6 +76,7 @@ en: user: "Account" debate: "Debate" proposal: "Proposal" + spending_proposal: "Spending proposal" verification::sms: "Telephone" verification::letter: "the verification" application: @@ -244,6 +246,11 @@ en: update: form: submit_button: "Save changes" + spending_proposals: + index: + title: "Citizen budget" + text: "Here you can send spending proposals to be considered in the frame of the annual citizen budgets." + create_link: "Create spending proposal" comments: show: return_to_commentable: "Go back to " diff --git a/config/locales/es.yml b/config/locales/es.yml index 37be2e03a..e67f42153 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -29,6 +29,7 @@ es: more_information: "Más información" debates: "Debates" proposals: "Propuestas" + spending_proposals: "Presupuestos ciudadanos" new_notifications: one: "Tienes una nueva notificación" other: "Tienes %{count} notificaciones nuevas" @@ -75,6 +76,7 @@ es: user: "la cuenta" debate: "el debate" proposal: "la propuesta" + spending_proposal: "la propuesta de gasto" verification::sms: "el teléfono" verification::letter: "la verificación" application: @@ -244,6 +246,11 @@ es: update: form: submit_button: "Guardar cambios" + spending_proposals: + index: + title: "Presupuestos ciudadanos" + text: "Desde esta sección podrás sugerir propuestas de gasto que irán asociadas a las partidas de presupuestos ciudadanos. El requisito principal es que sean propuestas presupuestables." + create_link: "Enviar propuesta de gasto" comments: show: return_to_commentable: "Volver a " diff --git a/config/routes.rb b/config/routes.rb index bfdecc59f..fa4edc5a1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -61,6 +61,8 @@ Rails.application.routes.draw do end end + resources :spending_proposals, only: [:index, :new, :create] + resources :legislations, only: [:show] resources :annotations do diff --git a/spec/features/spending_proposals_spec.rb b/spec/features/spending_proposals_spec.rb new file mode 100644 index 000000000..1a5946b01 --- /dev/null +++ b/spec/features/spending_proposals_spec.rb @@ -0,0 +1,11 @@ +require 'rails_helper' + +feature 'Spending proposals' do + + scenario 'Index' do + visit spending_proposals_path + + expect(page).to have_link('Create spending proposal', href: new_spending_proposal_path) + end + +end From c9950f8c115b2d89134dc8a30b5775e2a25c61f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Mon, 4 Jan 2016 17:48:02 +0100 Subject: [PATCH 26/64] activate header menus for all actions --- app/views/layouts/_header.html.erb | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb index 373871aff..a48ff8dbd 100644 --- a/app/views/layouts/_header.html.erb +++ b/app/views/layouts/_header.html.erb @@ -33,15 +33,16 @@
      -
      - <%= f.label :tag_list, t("spending_proposals.form.tags_label") %> -

      <%= t("spending_proposals.form.tags_instructions") %>

      - - <% @featured_tags.each do |tag| %> - <%= tag.name %> - <% end %> - - <%= f.text_field :tag_list, value: @spending_proposal.tag_list.to_s, label: false, placeholder: t("spending_proposals.form.tags_placeholder"), class: 'js-tag-list' %> -
      -
      <% if @spending_proposal.new_record? %> <%= f.label :terms_of_service do %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 60032de69..f66bba371 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -263,9 +263,6 @@ en: title: "Spending proposal title" description: "Description" external_url: "Link to additional documentation" - tags_label: "Tags" - tags_instructions: "Tag this spending proposal. You can choose from our tags or add your own." - tags_placeholder: "Enter the tags you would like to use, separated by commas (',')" submit_buttons: new: Create create: Create diff --git a/config/locales/es.yml b/config/locales/es.yml index da8511678..74464cfb4 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -263,9 +263,6 @@ es: title: "Título de la propuesta de gasto" description: "Descripción detallada" external_url: "Enlace a documentación adicional" - tags_label: "Temas" - tags_instructions: "Etiqueta esta propuesta de gasto. Puedes elegir entre nuestras sugerencias o introducir las que desees." - tags_placeholder: "Escribe las etiquetas que desees separadas por una coma (',')" submit_buttons: new: Crear create: Crear From bc8c14e4a1a391ad1f86effbcd4bad2c3c97a855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Thu, 7 Jan 2016 13:45:01 +0100 Subject: [PATCH 35/64] adds geozone to spending_proposals --- app/controllers/spending_proposals_controller.rb | 2 +- app/models/spending_proposal.rb | 1 + app/views/spending_proposals/_form.html.erb | 5 +++++ config/locales/en.yml | 3 +++ config/locales/es.yml | 3 +++ .../20160107114749_add_geozone_to_spending_proposal.rb | 6 ++++++ db/schema.rb | 2 ++ spec/features/spending_proposals_spec.rb | 1 + 8 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20160107114749_add_geozone_to_spending_proposal.rb diff --git a/app/controllers/spending_proposals_controller.rb b/app/controllers/spending_proposals_controller.rb index c182572d3..a7871549f 100644 --- a/app/controllers/spending_proposals_controller.rb +++ b/app/controllers/spending_proposals_controller.rb @@ -26,7 +26,7 @@ class SpendingProposalsController < ApplicationController private def spending_proposal_params - params.require(:spending_proposal).permit(:title, :description, :external_url, :terms_of_service, :captcha, :captcha_key) + params.require(:spending_proposal).permit(:title, :description, :external_url, :geozone_id, :terms_of_service, :captcha, :captcha_key) end end \ No newline at end of file diff --git a/app/models/spending_proposal.rb b/app/models/spending_proposal.rb index 2f73c2b33..f198bcb57 100644 --- a/app/models/spending_proposal.rb +++ b/app/models/spending_proposal.rb @@ -5,6 +5,7 @@ class SpendingProposal < ActiveRecord::Base apply_simple_captcha belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + belongs_to :geozone validates :title, presence: true validates :author, presence: true diff --git a/app/views/spending_proposals/_form.html.erb b/app/views/spending_proposals/_form.html.erb index 616cee986..39b0e6e0a 100644 --- a/app/views/spending_proposals/_form.html.erb +++ b/app/views/spending_proposals/_form.html.erb @@ -17,6 +17,11 @@ <%= f.text_field :external_url, placeholder: t("spending_proposals.form.external_url"), label: false %>
      +
      + <%= f.label :geozone_id, t("spending_proposals.form.geozone") %> + <%= f.select :geozone_id, Geozone.all.collect { |g| [ g.name, g.id ] }, {include_blank: t("geozones.none"), label: false} %> +
      +
      <% if @spending_proposal.new_record? %> <%= f.label :terms_of_service do %> diff --git a/config/locales/en.yml b/config/locales/en.yml index f66bba371..0f1284d95 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -263,9 +263,12 @@ en: title: "Spending proposal title" description: "Description" external_url: "Link to additional documentation" + geozone: "Area of operation" submit_buttons: new: Create create: Create + geozones: + none: "All city" comments: show: return_to_commentable: "Go back to " diff --git a/config/locales/es.yml b/config/locales/es.yml index 74464cfb4..9d0a07980 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -263,9 +263,12 @@ es: title: "Título de la propuesta de gasto" description: "Descripción detallada" external_url: "Enlace a documentación adicional" + geozone: "Nivel de actuación" submit_buttons: new: Crear create: Crear + geozones: + none: "Toda la ciudad" comments: show: return_to_commentable: "Volver a " diff --git a/db/migrate/20160107114749_add_geozone_to_spending_proposal.rb b/db/migrate/20160107114749_add_geozone_to_spending_proposal.rb new file mode 100644 index 000000000..62130fd9b --- /dev/null +++ b/db/migrate/20160107114749_add_geozone_to_spending_proposal.rb @@ -0,0 +1,6 @@ +class AddGeozoneToSpendingProposal < ActiveRecord::Migration + def change + add_column :spending_proposals, :geozone_id, :integer, default: nil + add_index :spending_proposals, :geozone_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 95d186822..0d9396440 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -288,9 +288,11 @@ ActiveRecord::Schema.define(version: 20160108133501) do t.string "external_url" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "geozone_id" end add_index "spending_proposals", ["author_id"], name: "index_spending_proposals_on_author_id", using: :btree + add_index "spending_proposals", ["geozone_id"], name: "index_spending_proposals_on_geozone_id", using: :btree create_table "taggings", force: :cascade do |t| t.integer "tag_id" diff --git a/spec/features/spending_proposals_spec.rb b/spec/features/spending_proposals_spec.rb index bf4a7f585..65b496fa9 100644 --- a/spec/features/spending_proposals_spec.rb +++ b/spec/features/spending_proposals_spec.rb @@ -17,6 +17,7 @@ feature 'Spending proposals' do fill_in 'spending_proposal_description', with: 'I want to live in a high tower over the clouds' fill_in 'spending_proposal_external_url', with: 'http://http://skyscraperpage.com/' fill_in 'spending_proposal_captcha', with: correct_captcha_text + select 'All city', from: 'spending_proposal_geozone_id' check 'spending_proposal_terms_of_service' click_button 'Create' From af0ef726fba7566a1ab511c594d5e81163ee4c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Thu, 7 Jan 2016 13:45:16 +0100 Subject: [PATCH 36/64] includes geozones in dev_seeds --- db/dev_seeds.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index d7a362d9a..ac94f569c 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -14,6 +14,9 @@ Setting.create(key: 'max_votes_for_proposal_edit', value: '1000') Setting.create(key: 'proposal_code_prefix', value: 'MAD') Setting.create(key: 'votes_for_proposal_success', value: '100') +puts "Creating Geozones" +('A'..'Z').each{ |i| Geozone.create(name: "District #{i}") } + puts "Creating Users" def create_user(email, username = Faker::Name.name) From 7b9285f93fc6844a9c06a45da85ab73b82198ca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Thu, 7 Jan 2016 13:45:43 +0100 Subject: [PATCH 37/64] defaults to order geozones by name --- app/models/geozone.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/geozone.rb b/app/models/geozone.rb index 303671493..9ad9aed61 100644 --- a/app/models/geozone.rb +++ b/app/models/geozone.rb @@ -1,3 +1,5 @@ class Geozone < ActiveRecord::Base validates :name, presence: true + + default_scope { order(name: :asc) } end From b3677fc044f31fa14fde1c5eaf3ace3e45065825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Thu, 7 Jan 2016 14:20:45 +0100 Subject: [PATCH 38/64] fixes typo --- config/locales/en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index 0f1284d95..c00f4935a 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -254,7 +254,7 @@ en: new: back_link: Back start_new: "Create spending proposal" - more_info: "How do participatory bufgeting works?" + more_info: "How do participatory budgeting works?" recommendations_title: "Recommendations for creating a spending proposal" recommendation_one: "It's mandatory that the proposal makes reference to a budgetable action." recommendation_two: "Any proposal or comment suggesting illegal action will be deleted." From facf6183d7cfc7ecda4533efbcb933e037a389bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Thu, 7 Jan 2016 14:56:02 +0100 Subject: [PATCH 39/64] adds resolution to spending proposals --- app/models/spending_proposal.rb | 12 +++++++++++- ...107132059_add_resolution_to_spending_proposals.rb | 6 ++++++ db/schema.rb | 2 ++ spec/models/spending_proposal_spec.rb | 12 ++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20160107132059_add_resolution_to_spending_proposals.rb diff --git a/app/models/spending_proposal.rb b/app/models/spending_proposal.rb index f198bcb57..b19ab4e2e 100644 --- a/app/models/spending_proposal.rb +++ b/app/models/spending_proposal.rb @@ -4,6 +4,8 @@ class SpendingProposal < ActiveRecord::Base apply_simple_captcha + RESOLUTIONS = ["accepted", "rejected"] + belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' belongs_to :geozone @@ -13,6 +15,14 @@ class SpendingProposal < ActiveRecord::Base validates :title, length: { in: 4..SpendingProposal.title_max_length } validates :description, length: { maximum: SpendingProposal.description_max_length } - + validates :resolution, inclusion: { in: RESOLUTIONS, allow_nil: true } validates :terms_of_service, acceptance: { allow_nil: false }, on: :create + + def accept + update_attribute(:resolution, "accepted") + end + + def reject + update_attribute(:resolution, "rejected") + end end diff --git a/db/migrate/20160107132059_add_resolution_to_spending_proposals.rb b/db/migrate/20160107132059_add_resolution_to_spending_proposals.rb new file mode 100644 index 000000000..18365a715 --- /dev/null +++ b/db/migrate/20160107132059_add_resolution_to_spending_proposals.rb @@ -0,0 +1,6 @@ +class AddResolutionToSpendingProposals < ActiveRecord::Migration + def change + add_column :spending_proposals, :resolution, :string, default: nil + add_index :spending_proposals, :resolution + end +end diff --git a/db/schema.rb b/db/schema.rb index 0d9396440..7bc4652d9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -289,10 +289,12 @@ ActiveRecord::Schema.define(version: 20160108133501) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "geozone_id" + t.string "resolution" end add_index "spending_proposals", ["author_id"], name: "index_spending_proposals_on_author_id", using: :btree add_index "spending_proposals", ["geozone_id"], name: "index_spending_proposals_on_geozone_id", using: :btree + add_index "spending_proposals", ["resolution"], name: "index_spending_proposals_on_resolution", using: :btree create_table "taggings", force: :cascade do |t| t.integer "tag_id" diff --git a/spec/models/spending_proposal_spec.rb b/spec/models/spending_proposal_spec.rb index e9b5f8ee4..ed99a2ecf 100644 --- a/spec/models/spending_proposal_spec.rb +++ b/spec/models/spending_proposal_spec.rb @@ -41,4 +41,16 @@ describe SpendingProposal do expect(spending_proposal).to_not be_valid end end + + describe "resolution status" do + it "can be accepted" do + spending_proposal.accept + expect(spending_proposal.reload.resolution).to eq("accepted") + end + + it "can be rejected" do + spending_proposal.reject + expect(spending_proposal.reload.resolution).to eq("rejected") + end + end end From 9a8dcd660fed7f6a3ca4f6eaa38513bfa0807481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Thu, 7 Jan 2016 15:10:01 +0100 Subject: [PATCH 40/64] adds resolution scopes --- app/models/spending_proposal.rb | 4 ++++ spec/models/spending_proposal_spec.rb | 33 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/app/models/spending_proposal.rb b/app/models/spending_proposal.rb index b19ab4e2e..e9ff0f728 100644 --- a/app/models/spending_proposal.rb +++ b/app/models/spending_proposal.rb @@ -18,6 +18,10 @@ class SpendingProposal < ActiveRecord::Base validates :resolution, inclusion: { in: RESOLUTIONS, allow_nil: true } validates :terms_of_service, acceptance: { allow_nil: false }, on: :create + scope :accepted, -> { where(resolution: "accepted") } + scope :rejected, -> { where(resolution: "rejected") } + scope :unresolved, -> { where(resolution: nil) } + def accept update_attribute(:resolution, "accepted") end diff --git a/spec/models/spending_proposal_spec.rb b/spec/models/spending_proposal_spec.rb index ed99a2ecf..d02d74eec 100644 --- a/spec/models/spending_proposal_spec.rb +++ b/spec/models/spending_proposal_spec.rb @@ -53,4 +53,37 @@ describe SpendingProposal do expect(spending_proposal.reload.resolution).to eq("rejected") end end + + describe "scopes" do + before(:each) do + 2.times { create(:spending_proposal, resolution: "accepted") } + 2.times { create(:spending_proposal, resolution: "rejected") } + 2.times { create(:spending_proposal, resolution: nil) } + end + + describe "unresolved" do + it "should return all spending proposals without resolution" do + unresolved = SpendingProposal.all.unresolved + expect(unresolved.size).to eq(2) + unresolved.each {|u| expect(u.resolution).to be_nil} + end + end + + describe "accepted" do + it "should return all accepted spending proposals" do + accepted = SpendingProposal.all.accepted + expect(accepted.size).to eq(2) + accepted.each {|a| expect(a.resolution).to eq("accepted")} + end + end + + describe "rejected" do + it "should return all rejected spending proposals" do + rejected = SpendingProposal.all.rejected + expect(rejected.size).to eq(2) + rejected.each {|r| expect(r.resolution).to eq("rejected")} + end + end + end + end From 7c92a92537e55f488a3dd6014d46af0a5bf5c9ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baza=CC=81n?= Date: Fri, 8 Jan 2016 12:11:24 +0100 Subject: [PATCH 41/64] adds resolution query methods --- app/models/spending_proposal.rb | 12 +++++++ spec/models/spending_proposal_spec.rb | 51 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/app/models/spending_proposal.rb b/app/models/spending_proposal.rb index e9ff0f728..157b5f586 100644 --- a/app/models/spending_proposal.rb +++ b/app/models/spending_proposal.rb @@ -29,4 +29,16 @@ class SpendingProposal < ActiveRecord::Base def reject update_attribute(:resolution, "rejected") end + + def accepted? + resolution == "accepted" + end + + def rejected? + resolution == "rejected" + end + + def unresolved? + resolution.blank? + end end diff --git a/spec/models/spending_proposal_spec.rb b/spec/models/spending_proposal_spec.rb index d02d74eec..25bccbfc6 100644 --- a/spec/models/spending_proposal_spec.rb +++ b/spec/models/spending_proposal_spec.rb @@ -43,6 +43,15 @@ describe SpendingProposal do end describe "resolution status" do + it "should be valid" do + spending_proposal.resolution = "accepted" + expect(spending_proposal).to be_valid + spending_proposal.resolution = "rejected" + expect(spending_proposal).to be_valid + spending_proposal.resolution = "wrong" + expect(spending_proposal).to_not be_valid + end + it "can be accepted" do spending_proposal.accept expect(spending_proposal.reload.resolution).to eq("accepted") @@ -52,6 +61,48 @@ describe SpendingProposal do spending_proposal.reject expect(spending_proposal.reload.resolution).to eq("rejected") end + + describe "#accepted?" do + it "should be true if resolution equals 'accepted'" do + spending_proposal.resolution = "accepted" + expect(spending_proposal.accepted?).to eq true + end + + it "should be false otherwise" do + spending_proposal.resolution = "rejected" + expect(spending_proposal.accepted?).to eq false + spending_proposal.resolution = nil + expect(spending_proposal.accepted?).to eq false + end + end + + describe "#rejected?" do + it "should be true if resolution equals 'rejected'" do + spending_proposal.resolution = "rejected" + expect(spending_proposal.rejected?).to eq true + end + + it "should be false otherwise" do + spending_proposal.resolution = "accepted" + expect(spending_proposal.rejected?).to eq false + spending_proposal.resolution = nil + expect(spending_proposal.rejected?).to eq false + end + end + + describe "#unresolved?" do + it "should be true if resolution is blank" do + spending_proposal.resolution = nil + expect(spending_proposal.unresolved?).to eq true + end + + it "should be false otherwise" do + spending_proposal.resolution = "accepted" + expect(spending_proposal.unresolved?).to eq false + spending_proposal.resolution = "rejected" + expect(spending_proposal.unresolved?).to eq false + end + end end describe "scopes" do From 3f05864a165c4bb4e9eb9361b8f482e0fe5a8cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baza=CC=81n?= Date: Mon, 11 Jan 2016 13:24:42 +0100 Subject: [PATCH 42/64] adds spending proposals to admin --- .../admin/spending_proposals_controller.rb | 26 +++++ app/views/admin/_menu.html.erb | 7 ++ .../admin/spending_proposals/index.html.erb | 33 ++++++ config/i18n-tasks.yml | 1 + config/locales/admin.en.yml | 12 +++ config/locales/admin.es.yml | 12 +++ config/routes.rb | 7 ++ .../features/admin/spending_proposals_spec.rb | 100 ++++++++++++++++++ 8 files changed, 198 insertions(+) create mode 100644 app/controllers/admin/spending_proposals_controller.rb create mode 100644 app/views/admin/spending_proposals/index.html.erb create mode 100644 spec/features/admin/spending_proposals_spec.rb diff --git a/app/controllers/admin/spending_proposals_controller.rb b/app/controllers/admin/spending_proposals_controller.rb new file mode 100644 index 000000000..dae595716 --- /dev/null +++ b/app/controllers/admin/spending_proposals_controller.rb @@ -0,0 +1,26 @@ +class Admin::SpendingProposalsController < Admin::BaseController + has_filters %w{unresolved accepted rejected}, only: :index + + before_action :load_spending_proposal, except: [:index] + + def index + @spending_proposals = SpendingProposal.send(@current_filter).order(created_at: :desc).page(params[:page]) + end + + def accept + @spending_proposal.accept + redirect_to request.query_parameters.merge(action: :index) + end + + def reject + @spending_proposal.reject + redirect_to request.query_parameters.merge(action: :index) + end + + private + + def load_spending_proposal + @spending_proposal = SpendingProposal.find(params[:id]) + end + +end \ No newline at end of file diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index 5f45e96c8..b27a8a73b 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -32,6 +32,13 @@ <% end %> +
    • > + <%= link_to admin_spending_proposals_path do %> + + <%= t("admin.menu.spending_proposals") %> + <% end %> +
    • +
    • > <%= link_to admin_users_path do %> diff --git a/app/views/admin/spending_proposals/index.html.erb b/app/views/admin/spending_proposals/index.html.erb new file mode 100644 index 000000000..cf55594d0 --- /dev/null +++ b/app/views/admin/spending_proposals/index.html.erb @@ -0,0 +1,33 @@ +

      <%= t("admin.spending_proposals.index.title") %>

      + +<%= render 'shared/filter_subnav', i18n_namespace: "admin.spending_proposals.index" %> + +

      <%= page_entries_info @spending_proposals %>

      + + + <% @spending_proposals.each do |spending_proposal| %> + + + + + <% end %> +
      + <%= spending_proposal.title %> + + <% unless spending_proposal.accepted? %> + <%= link_to t("admin.spending_proposals.actions.accept"), + accept_admin_spending_proposal_path(spending_proposal, request.query_parameters), + method: :put, + data: { confirm: t("admin.actions.confirm") }, + class: "button radius tiny success no-margin" %> + <% end %> + <% unless spending_proposal.rejected? %> + <%= link_to t("admin.spending_proposals.actions.reject"), + reject_admin_spending_proposal_path(spending_proposal, request.query_parameters), + method: :put, + data: { confirm: t("admin.actions.confirm") }, + class: "button radius tiny warning right" %> + <% end %> +
      + +<%= paginate @spending_proposals %> diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 6b815cd31..9941114f8 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -109,6 +109,7 @@ ignore_unused: - 'admin.comments.index.filter*' - 'admin.debates.index.filter*' - 'admin.proposals.index.filter*' + - 'admin.spending_proposals.index.filter*' - 'admin.organizations.index.filter*' - 'admin.users.index.filter*' - 'admin.activity.show.filter*' diff --git a/config/locales/admin.en.yml b/config/locales/admin.en.yml index 276e3ca96..6b421a6b1 100755 --- a/config/locales/admin.en.yml +++ b/config/locales/admin.en.yml @@ -16,6 +16,7 @@ en: hidden_debates: "Hidden debates" hidden_comments: "Hidden comments" hidden_users: "Hidden users" + spending_proposals: "Spending proposals" incomplete_verifications: "Incomplete verifications" organizations: "Organisations" officials: "Officials" @@ -91,6 +92,17 @@ en: all: "All" with_confirmed_hide: "Confirmed" without_confirmed_hide: "Pending" + spending_proposals: + actions: + accept: Accept + reject: Reject + index: + title: "Spending proposals for participatory budgeting" + filter: "Filter" + filters: + unresolved: "Unresolved" + accepted: "Accepted" + rejected: "Rejected" users: index: title: "Hidden users" diff --git a/config/locales/admin.es.yml b/config/locales/admin.es.yml index b124d60e3..9f7099996 100644 --- a/config/locales/admin.es.yml +++ b/config/locales/admin.es.yml @@ -16,6 +16,7 @@ es: hidden_debates: "Debates ocultos" hidden_comments: "Comentarios ocultos" hidden_users: "Usuarios bloqueados" + spending_proposals: "Propuestas de gasto" incomplete_verifications: "Verificaciones incompletas" organizations: "Organizaciones" officials: "Cargos públicos" @@ -91,6 +92,17 @@ es: all: "Todas" with_confirmed_hide: "Confirmadas" without_confirmed_hide: "Pendientes" + spending_proposals: + actions: + accept: Aceptar + reject: Rechazar + index: + title: "Propuestas de gasto para presupuestos participativos" + filter: "Filtro" + filters: + unresolved: "Sin resolver" + accepted: "Aceptadas" + rejected: "Rechazadas" users: index: title: "Usuarios bloqueados" diff --git a/config/routes.rb b/config/routes.rb index fa4edc5a1..c2b776dec 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -122,6 +122,13 @@ Rails.application.routes.draw do end end + resources :spending_proposals, only: :index do + member do + put :accept + put :reject + end + end + resources :comments, only: :index do member do put :restore diff --git a/spec/features/admin/spending_proposals_spec.rb b/spec/features/admin/spending_proposals_spec.rb new file mode 100644 index 000000000..f684bf386 --- /dev/null +++ b/spec/features/admin/spending_proposals_spec.rb @@ -0,0 +1,100 @@ +require 'rails_helper' + +feature 'Admin spending proposals' do + + background do + admin = create(:administrator) + login_as(admin.user) + end + + scenario 'Index shows spending proposals' do + spending_proposal = create(:spending_proposal) + visit admin_spending_proposals_path + + expect(page).to have_content(spending_proposal.title) + end + + scenario 'Accept' do + spending_proposal = create(:spending_proposal) + visit admin_spending_proposals_path + + click_link 'Accept' + + expect(page).to_not have_content(spending_proposal.title) + + click_link 'Accepted' + expect(page).to have_content(spending_proposal.title) + + expect(spending_proposal.reload).to be_accepted + end + + scenario 'Reject' do + spending_proposal = create(:spending_proposal) + visit admin_spending_proposals_path + + click_link 'Reject' + + expect(page).to_not have_content(spending_proposal.title) + + click_link('Rejected') + expect(page).to have_content(spending_proposal.title) + + expect(spending_proposal.reload).to be_rejected + end + + scenario "Current filter is properly highlighted" do + visit admin_spending_proposals_path + expect(page).to_not have_link('Unresolved') + expect(page).to have_link('Accepted') + expect(page).to have_link('Rejected') + + visit admin_spending_proposals_path(filter: 'unresolved') + expect(page).to_not have_link('Unresolved') + expect(page).to have_link('Accepted') + expect(page).to have_link('Rejected') + + visit admin_spending_proposals_path(filter: 'accepted') + expect(page).to have_link('Unresolved') + expect(page).to_not have_link('Accepted') + expect(page).to have_link('Rejected') + + visit admin_spending_proposals_path(filter: 'rejected') + expect(page).to have_link('Accepted') + expect(page).to have_link('Unresolved') + expect(page).to_not have_link('Rejected') + end + + scenario "Filtering proposals" do + create(:spending_proposal, title: "Recent spending proposal") + create(:spending_proposal, title: "Good spending proposal", resolution: "accepted") + create(:spending_proposal, title: "Bad spending proposal", resolution: "rejected") + + visit admin_spending_proposals_path(filter: 'unresolved') + expect(page).to have_content('Recent spending proposal') + expect(page).to_not have_content('Good spending proposal') + expect(page).to_not have_content('Bad spending proposal') + + visit admin_spending_proposals_path(filter: 'accepted') + expect(page).to have_content('Good spending proposal') + expect(page).to_not have_content('Recent spending proposal') + expect(page).to_not have_content('Bad spending proposal') + + visit admin_spending_proposals_path(filter: 'rejected') + expect(page).to have_content('Bad spending proposal') + expect(page).to_not have_content('Good spending proposal') + expect(page).to_not have_content('Recent spending proposal') + end + + scenario "Action links remember the pagination setting and the filter" do + per_page = Kaminari.config.default_per_page + (per_page + 2).times { create(:spending_proposal, resolution: "accepted") } + + visit admin_spending_proposals_path(filter: 'accepted', page: 2) + + click_on('Reject', match: :first, exact: true) + + expect(current_url).to include('filter=accepted') + expect(current_url).to include('page=2') + end + +end From 0510d211b3bf03bd245a3a91a6534e6089eab896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Mon, 11 Jan 2016 13:45:15 +0100 Subject: [PATCH 43/64] adds spending resolutions to dev_seeds --- db/dev_seeds.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index ac94f569c..9c574f45d 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -183,6 +183,23 @@ end Flag.flag(flagger, proposal) end +puts "Creating Spending Proposals" + +resolutions = ["accepted", "rejected", nil] + +(1..30).each do |i| + author = User.reorder("RANDOM()").first + description = "

      #{Faker::Lorem.paragraphs.join('

      ')}

      " + spending_proposal = SpendingProposal.create!(author: author, + title: Faker::Lorem.sentence(3).truncate(60), + external_url: Faker::Internet.url, + description: description, + created_at: rand((Time.now - 1.week) .. Time.now), + resolution: resolutions.sample, + terms_of_service: "1") + puts " #{spending_proposal.title}" +end + puts "Creating Legislation" Legislation.create!(title: 'Participatory Democracy', body: 'In order to achieve...') From 51e44357ffe6b6e7855b9f603c3143858053542b Mon Sep 17 00:00:00 2001 From: Alberto Garcia Cabeza Date: Mon, 11 Jan 2016 13:51:29 +0100 Subject: [PATCH 44/64] Fixes styles for filters on admin pages --- app/assets/stylesheets/layout.scss | 21 ++++++++++----------- app/views/shared/_filter_subnav.html.erb | 2 +- app/views/users/show.html.erb | 2 +- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index 650621624..4e92fb7b5 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -220,6 +220,16 @@ a { } } +.sub-nav { + border-bottom: 1px solid $border; + + dd.active { + border-bottom: 2px solid $brand; + color: $brand; + padding-bottom: $line-height/4; + } +} + // 02. Header // - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1802,17 +1812,6 @@ table { .activity { margin-bottom: $line-height*2; - margin-top: $line-height; - - .sub-nav { - border-bottom: 1px solid $border; - - dd.active { - border-bottom: 2px solid $brand; - color: $brand; - padding-bottom: $line-height/4; - } - } table { border: 0; diff --git a/app/views/shared/_filter_subnav.html.erb b/app/views/shared/_filter_subnav.html.erb index 0bf9f773c..3607290af 100644 --- a/app/views/shared/_filter_subnav.html.erb +++ b/app/views/shared/_filter_subnav.html.erb @@ -4,7 +4,7 @@ %>