diff --git a/app/assets/fonts/icons.eot b/app/assets/fonts/icons.eot
index 3046c50cc..ced98a9b4 100644
Binary files a/app/assets/fonts/icons.eot and b/app/assets/fonts/icons.eot differ
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 b933f1c3f..f3486cf96 100644
Binary files a/app/assets/fonts/icons.ttf and b/app/assets/fonts/icons.ttf differ
diff --git a/app/assets/fonts/icons.woff b/app/assets/fonts/icons.woff
index feb047bd0..9045aa563 100644
Binary files a/app/assets/fonts/icons.woff and b/app/assets/fonts/icons.woff differ
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";
+}
diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss
index 62226e967..650621624 100644
--- a/app/assets/stylesheets/layout.scss
+++ b/app/assets/stylesheets/layout.scss
@@ -396,7 +396,6 @@ header {
&:hover {
background: none;
color: white;
- text-decoration: underline;
transition: text-decoration 275ms;
}
@@ -420,6 +419,7 @@ header {
&:hover, &:focus {
background-color: #007095 !important;
+ text-decoration: underline;
}
}
@@ -947,6 +947,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
// - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -1574,7 +1645,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;
@@ -1595,15 +1665,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/assets/stylesheets/variables.scss b/app/assets/stylesheets/variables.scss
index d9da9c580..6a61ec72c 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/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 841b32498..5150f97ec 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -108,4 +108,5 @@ class ApplicationController < ActionController::Base
store_location_for(:user, request.path)
end
end
+
end
diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb
index 35406faf0..df928aa02 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
@@ -9,11 +9,17 @@ class CommentsController < ApplicationController
def create
if @comment.save
CommentNotifier.new(comment: @comment).process
+ add_notification @comment
else
render :new
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
@@ -62,4 +68,13 @@ class CommentsController < ApplicationController
["1", true].include?(comment_params[:as_moderator]) && can?(:comment_as_moderator, @commentable)
end
+ def add_notification(comment)
+ if comment.reply?
+ notifiable = comment.parent
+ else
+ notifiable = comment.commentable
+ end
+ Notification.add(notifiable.author_id, notifiable) unless comment.author_id == notifiable.author_id
+ end
+
end
diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb
new file mode 100644
index 000000000..a4ec31b50
--- /dev/null
+++ b/app/controllers/notifications_controller.rb
@@ -0,0 +1,26 @@
+class NotificationsController < ApplicationController
+ before_action :authenticate_user!
+ after_action :mark_as_read, only: :show
+ skip_authorization_check
+
+ def index
+ @notifications = current_user.notifications.unread.recent.for_render
+ end
+
+ def show
+ @notification = current_user.notifications.find(params[:id])
+ redirect_to url_for(@notification.notifiable)
+ 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/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/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
new file mode 100644
index 000000000..281163380
--- /dev/null
+++ b/app/helpers/notifications_helper.rb
@@ -0,0 +1,6 @@
+module NotificationsHelper
+
+ def notification_action(notification)
+ notification.notifiable_type == "Comment" ? "replies_to" : "comments_on"
+ end
+end
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/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/notification.rb b/app/models/notification.rb
new file mode 100644
index 000000000..1cb500ccf
--- /dev/null
+++ b/app/models/notification.rb
@@ -0,0 +1,24 @@
+class Notification < ActiveRecord::Base
+ belongs_to :user, counter_cache: true
+ belongs_to :notifiable, polymorphic: true
+
+ scope :unread, -> { all }
+ scope :recent, -> { order(id: :desc) }
+ scope :for_render, -> { includes(:notifiable) }
+
+ def timestamp
+ notifiable.created_at
+ end
+
+ 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/models/user.rb b/app/models/user.rb
index dd2bc5cf0..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?
@@ -199,7 +200,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/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb
index 9178f0b9b..e8c49636b 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 %>
<%= t("comments.comment.deleted") %>
<% 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)) %>
diff --git a/app/views/notifications/_notification.html.erb b/app/views/notifications/_notification.html.erb
new file mode 100644
index 000000000..28def42f7
--- /dev/null
+++ b/app/views/notifications/_notification.html.erb
@@ -0,0 +1,9 @@
+-
+ <%= link_to notification do %>
+
\ No newline at end of file
diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb
new file mode 100644
index 000000000..cc9170e48
--- /dev/null
+++ b/app/views/notifications/index.html.erb
@@ -0,0 +1,18 @@
+
+
+ <% 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 %>
+
+
+
+
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index 561ad3347..6b815cd31 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -127,6 +127,8 @@ ignore_unused:
- 'proposals.index.select_order'
- 'proposals.index.orders.*'
- 'proposals.index.search_form.*'
+ - 'notifications.index.comments_on*'
+ - 'notifications.index.replies_to*'
- 'helpers.page_entries_info.*' # kaminari
- 'views.pagination.*' # kaminari
# - '{devise,kaminari,will_paginate}.*'
diff --git a/config/locales/en.yml b/config/locales/en.yml
index f8edcd12a..c40575d6b 100755
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -29,6 +29,10 @@ en:
more_information: "More information"
debates: "Debates"
proposals: "Proposals"
+ 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."
open_source: "open-source software"
@@ -241,6 +245,8 @@ en:
form:
submit_button: "Save changes"
comments:
+ show:
+ return_to_commentable: "Go back to "
select_order: "Sort by"
orders:
most_voted: "Most voted"
@@ -307,6 +313,16 @@ 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: "You don't have new notifications."
+ 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 8d2a2c2f7..37be2e03a 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -29,6 +29,10 @@ es:
more_information: "Más información"
debates: "Debates"
proposals: "Propuestas"
+ 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."
open_source: "software libre"
@@ -241,6 +245,8 @@ es:
form:
submit_button: "Guardar cambios"
comments:
+ show:
+ return_to_commentable: "Volver a "
select_order: "Ordenar por"
orders:
most_voted: "Más votados"
@@ -307,6 +313,16 @@ 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 tienes notificaciones nuevas."
+ 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/config/routes.rb b/config/routes.rb
index 3472f1c4e..bfdecc59f 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
@@ -74,6 +74,11 @@ Rails.application.routes.draw do
resource :account, controller: "account", only: [:show, :update, :delete] do
collection { get :erase }
end
+
+ resources :notifications, only: [:index, :show] do
+ collection { put :mark_all_as_read }
+ end
+
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/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/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/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 ca180b297..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: 20151215165824) do
+ActiveRecord::Schema.define(version: 20160108133501) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -204,6 +204,15 @@ 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 "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
+
create_table "organizations", force: :cascade do |t|
t.integer "user_id"
t.string "name", limit: 60
@@ -330,6 +339,7 @@ ActiveRecord::Schema.define(version: 20151215165824) 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
@@ -404,5 +414,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", "users"
add_foreign_key "organizations", "users"
end
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
diff --git a/spec/factories.rb b/spec/factories.rb
index 2c20000ca..6e910a214 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -290,4 +290,9 @@ FactoryGirl.define do
sequence(:track_id) { |n| "#{n}" }
end
+ factory :notification do
+ user
+ association :notifiable, factory: :proposal
+ end
+
end
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/features/notifications_spec.rb b/spec/features/notifications_spec.rb
new file mode 100644
index 000000000..1ffc6a854
--- /dev/null
+++ b/spec/features/notifications_spec.rb
@@ -0,0 +1,192 @@
+require 'rails_helper'
+
+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
+ 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
+
+ find(".icon-notification").click
+
+ expect(page).to have_css ".notification", count: 1
+
+ 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
+
+ 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
+
+ find(".icon-notification").click
+
+ expect(page).to have_css ".notification", count: 1
+ 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
+
+ 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
+
+ find(".icon-no-notification").click
+ expect(page).to have_css ".notification", count: 0
+ 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
+
+ find(".icon-no-notification")
+
+ visit notifications_path
+ 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 "You don't have new notifications"
+ end
+
+end
diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb
new file mode 100644
index 000000000..ccb3b3a9d
--- /dev/null
+++ b/spec/helpers/notifications_helper_spec.rb
@@ -0,0 +1,25 @@
+require 'rails_helper'
+
+describe NotificationsHelper do
+
+ describe "#notification_action" do
+ let(:debate) { create :debate }
+ let(:debate_comment) { create :comment, commentable: debate }
+
+ 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
+ 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: debate_comment
+ expect(notification_action(notification)).to eq "replies_to"
+ end
+ end
+ end
+
+
+end
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
diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb
index 11a02400a..73b70fb42 100644
--- a/spec/models/comment_spec.rb
+++ b/spec/models/comment_spec.rb
@@ -128,4 +128,5 @@ describe Comment do
expect(Comment.not_as_admin_or_moderator.first).to eq(comment1)
end
end
+
end
diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb
new file mode 100644
index 000000000..361211a51
--- /dev/null
+++ b/spec/models/notification_spec.rb
@@ -0,0 +1,50 @@
+require 'rails_helper'
+
+describe Notification do
+
+ describe "#unread (scope)" do
+ it "returns only unread notifications" do
+ 2.times { create :notification }
+ expect(Notification.unread.size).to be 2
+ 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 including notifiable and user" do
+ expect(Notification).to receive(:includes).with(:notifiable).exactly(:once)
+ Notification.for_render
+ end
+ end
+
+ describe "#timestamp" do
+ it "returns the timestamp of the trackable object" do
+ comment = create :comment
+ notification = create :notification, notifiable: comment
+
+ expect(notification.timestamp).to eq comment.created_at
+ end
+ end
+
+ describe "#mark_as_read" do
+ it "destroys notification" do
+ notification = create :notification
+ expect(Notification.unread.size).to eq 1
+
+ notification.mark_as_read
+ expect(Notification.unread.size).to eq 0
+ end
+ end
+
+end
+ <%= 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 %> ++ <%= render @notifications %> +
+ <% end %> +