Merge pull request #2549 from consul/read_notifications
Extend notifications to be marked as read or unread
This commit is contained in:
@@ -174,6 +174,13 @@ a {
|
|||||||
margin-right: $line-height;
|
margin-right: $line-height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.float-right-medium {
|
||||||
|
|
||||||
|
@include breakpoint(medium) {
|
||||||
|
float: right !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.no-margin-top {
|
.no-margin-top {
|
||||||
margin-top: rem-calc(-24);
|
margin-top: rem-calc(-24);
|
||||||
}
|
}
|
||||||
@@ -1169,21 +1176,46 @@ form {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notifications-list::before {
|
.notifications-list {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
background: $border;
|
background: $border;
|
||||||
content: '';
|
content: '';
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: 22px;
|
left: 7px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 60px;
|
top: 2px;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification {
|
.notification {
|
||||||
|
border: 1px solid $border;
|
||||||
display: block;
|
display: block;
|
||||||
padding: $line-height / 2 0 $line-height / 2 $line-height * 1.5;
|
margin-bottom: $line-height / 4;
|
||||||
|
margin-left: $line-height;
|
||||||
|
padding: $line-height / 2 $line-height;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@include breakpoint(medium) {
|
||||||
|
|
||||||
|
.mark-notification {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unread {
|
||||||
|
background: $highlight-soft;
|
||||||
|
border: 1px solid $highlight;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $highlight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@@ -1204,13 +1236,19 @@ form {
|
|||||||
color: $brand;
|
color: $brand;
|
||||||
content: '\4d';
|
content: '\4d';
|
||||||
font-family: "icons" !important;
|
font-family: "icons" !important;
|
||||||
left: 0;
|
font-size: $small-font-size;
|
||||||
|
height: rem-calc(20);
|
||||||
|
left: -24px;
|
||||||
|
line-height: rem-calc(20);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
width: rem-calc(20);
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
color: $text;
|
color: $text;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
max-width: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time {
|
.time {
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class CommentsController < ApplicationController
|
|||||||
notifiable = comment.reply? ? comment.parent : comment.commentable
|
notifiable = comment.reply? ? comment.parent : comment.commentable
|
||||||
notifiable_author_id = notifiable.try(:author_id)
|
notifiable_author_id = notifiable.try(:author_id)
|
||||||
if notifiable_author_id.present? && notifiable_author_id != comment.author_id
|
if notifiable_author_id.present? && notifiable_author_id != comment.author_id
|
||||||
Notification.add(notifiable.author_id, notifiable)
|
Notification.add(notifiable.author, notifiable)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -2,29 +2,41 @@ class NotificationsController < ApplicationController
|
|||||||
include CustomUrlsHelper
|
include CustomUrlsHelper
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
after_action :mark_as_read, only: :show
|
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
respond_to :html, :js
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@notifications = current_user.notifications.unread.recent.for_render
|
@notifications = current_user.notifications.unread
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@notification = current_user.notifications.find(params[:id])
|
@notification = current_user.notifications.find(params[:id])
|
||||||
|
@notification.mark_as_read
|
||||||
redirect_to linkable_resource_path(@notification)
|
redirect_to linkable_resource_path(@notification)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def read
|
||||||
|
@notifications = current_user.notifications.read
|
||||||
|
end
|
||||||
|
|
||||||
def mark_all_as_read
|
def mark_all_as_read
|
||||||
current_user.notifications.each { |notification| notification.mark_as_read }
|
current_user.notifications.unread.each { |notification| notification.mark_as_read }
|
||||||
redirect_to notifications_path
|
redirect_to notifications_path
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def mark_as_read
|
def mark_as_read
|
||||||
|
@notification = current_user.notifications.find(params[:id])
|
||||||
@notification.mark_as_read
|
@notification.mark_as_read
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def mark_as_unread
|
||||||
|
@notification = current_user.notifications.find(params[:id])
|
||||||
|
@notification.mark_as_unread
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
def linkable_resource_path(notification)
|
def linkable_resource_path(notification)
|
||||||
case notification.linkable_resource.class.name
|
case notification.linkable_resource.class.name
|
||||||
when "Budget::Investment"
|
when "Budget::Investment"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class ProposalNotificationsController < ApplicationController
|
|||||||
@proposal = Proposal.find(proposal_notification_params[:proposal_id])
|
@proposal = Proposal.find(proposal_notification_params[:proposal_id])
|
||||||
if @notification.save
|
if @notification.save
|
||||||
@proposal.users_to_notify.each do |user|
|
@proposal.users_to_notify.each do |user|
|
||||||
Notification.add(user.id, @notification)
|
Notification.add(user, @notification)
|
||||||
end
|
end
|
||||||
redirect_to @notification, notice: I18n.t("flash.actions.create.proposal_notification")
|
redirect_to @notification, notice: I18n.t("flash.actions.create.proposal_notification")
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -3,32 +3,50 @@ class Notification < ActiveRecord::Base
|
|||||||
belongs_to :user, counter_cache: true
|
belongs_to :user, counter_cache: true
|
||||||
belongs_to :notifiable, polymorphic: true
|
belongs_to :notifiable, polymorphic: true
|
||||||
|
|
||||||
scope :unread, -> { all }
|
validates :user, presence: true
|
||||||
scope :recent, -> { order(id: :desc) }
|
|
||||||
|
scope :read, -> { where.not(read_at: nil).recent.for_render }
|
||||||
|
scope :unread, -> { where(read_at: nil).recent.for_render }
|
||||||
scope :not_emailed, -> { where(emailed_at: nil) }
|
scope :not_emailed, -> { where(emailed_at: nil) }
|
||||||
|
scope :recent, -> { order(id: :desc) }
|
||||||
scope :for_render, -> { includes(:notifiable) }
|
scope :for_render, -> { includes(:notifiable) }
|
||||||
|
|
||||||
delegate :notifiable_title, :notifiable_available?, :check_availability, :linkable_resource,
|
delegate :notifiable_title, :notifiable_available?, :check_availability,
|
||||||
to: :notifiable, allow_nil: true
|
:linkable_resource, to: :notifiable, allow_nil: true
|
||||||
|
|
||||||
|
def mark_as_read
|
||||||
|
update(read_at: Time.current)
|
||||||
|
end
|
||||||
|
|
||||||
|
def mark_as_unread
|
||||||
|
update(read_at: nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def read?
|
||||||
|
read_at.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def unread?
|
||||||
|
read_at.nil?
|
||||||
|
end
|
||||||
|
|
||||||
def timestamp
|
def timestamp
|
||||||
notifiable.created_at
|
notifiable.created_at
|
||||||
end
|
end
|
||||||
|
|
||||||
def mark_as_read
|
def self.add(user, notifiable)
|
||||||
destroy
|
notification = Notification.existent(user, notifiable)
|
||||||
end
|
|
||||||
|
|
||||||
def self.add(user_id, notifiable)
|
|
||||||
notification = Notification.find_by(user_id: user_id, notifiable: notifiable)
|
|
||||||
|
|
||||||
if notification.present?
|
if notification.present?
|
||||||
Notification.increment_counter(:counter, notification.id)
|
increment_counter(:counter, notification.id)
|
||||||
else
|
else
|
||||||
Notification.create!(user_id: user_id, notifiable: notifiable)
|
create!(user: user, notifiable: notifiable)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.existent(user, notifiable)
|
||||||
|
unread.where(user: user, notifiable: notifiable).first
|
||||||
|
end
|
||||||
|
|
||||||
def notifiable_action
|
def notifiable_action
|
||||||
case notifiable_type
|
case notifiable_type
|
||||||
when "ProposalNotification"
|
when "ProposalNotification"
|
||||||
|
|||||||
@@ -1,22 +1,4 @@
|
|||||||
<% if user_signed_in? %>
|
<% if user_signed_in? %>
|
||||||
<li id="notifications">
|
|
||||||
<%= link_to notifications_path, rel: "nofollow", class: "notifications" do %>
|
|
||||||
<span class="show-for-sr"><%= t("layouts.header.notifications") %></span>
|
|
||||||
<% if current_user.notifications_count > 0 %>
|
|
||||||
<span class="icon-circle" aria-hidden="true"></span>
|
|
||||||
<span class="icon-notification" aria-hidden="true" title="<%= t('layouts.header.new_notifications', count: current_user.notifications_count).html_safe %>">
|
|
||||||
</span>
|
|
||||||
<small class="show-for-small-only">
|
|
||||||
<%= t('layouts.header.new_notifications', count: current_user.notifications_count).html_safe %>
|
|
||||||
</small>
|
|
||||||
<% else %>
|
|
||||||
<span class="icon-no-notification" aria-hidden="true" title="<%= t('layouts.header.no_notifications') %>"></span>
|
|
||||||
<small class="show-for-small-only">
|
|
||||||
<%= t('layouts.header.no_notifications') %>
|
|
||||||
</small>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<%= layout_menu_link_to t("layouts.header.my_activity_link"),
|
<%= layout_menu_link_to t("layouts.header.my_activity_link"),
|
||||||
user_path(current_user),
|
user_path(current_user),
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
<ul class="menu" data-responsive-menu="medium-dropdown">
|
<ul class="menu" data-responsive-menu="medium-dropdown">
|
||||||
<%= render "admin/shared/admin_shortcuts" %>
|
<%= render "admin/shared/admin_shortcuts" %>
|
||||||
<%= render "shared/admin_login_items" %>
|
<%= render "shared/admin_login_items" %>
|
||||||
|
<%= render "layouts/notification_item" %>
|
||||||
<%= render "devise/menu/login_items" %>
|
<%= render "devise/menu/login_items" %>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
<div class="top-bar-right">
|
<div class="top-bar-right">
|
||||||
<ul class="menu" data-responsive-menu="medium-dropdown">
|
<ul class="menu" data-responsive-menu="medium-dropdown">
|
||||||
<%= render "shared/admin_login_items" %>
|
<%= render "shared/admin_login_items" %>
|
||||||
|
<%= render "layouts/notification_item" %>
|
||||||
<%= render "devise/menu/login_items" %>
|
<%= render "devise/menu/login_items" %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|||||||
30
app/views/layouts/_notification_item.html.erb
Normal file
30
app/views/layouts/_notification_item.html.erb
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<% if user_signed_in? %>
|
||||||
|
<li id="notifications">
|
||||||
|
<%= link_to notifications_path, rel: "nofollow",
|
||||||
|
class: "notifications" do %>
|
||||||
|
<span class="show-for-sr">
|
||||||
|
<%= t("layouts.header.notification_item.notifications") %>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<% if current_user.notifications.unread.count > 0 %>
|
||||||
|
<span class="icon-circle" aria-hidden="true"></span>
|
||||||
|
<span class="icon-notification" aria-hidden="true"
|
||||||
|
title="<%= t('layouts.header.notification_item.new_notifications',
|
||||||
|
count: current_user.notifications_count).html_safe %>">
|
||||||
|
</span>
|
||||||
|
<small class="show-for-small-only">
|
||||||
|
<%= t('layouts.header.notification_item.new_notifications',
|
||||||
|
count: current_user.notifications_count).html_safe %>
|
||||||
|
</small>
|
||||||
|
<% else %>
|
||||||
|
<span class="icon-no-notification" aria-hidden="true"
|
||||||
|
title="<%= t('layouts.header.notification_item.no_notifications') %>">
|
||||||
|
</span>
|
||||||
|
<small class="show-for-small-only">
|
||||||
|
<%= t('layouts.header.notification_item.no_notifications') %>
|
||||||
|
</small>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
@@ -1,21 +1,38 @@
|
|||||||
<li id="<%= dom_id(notification) %>" class="notification">
|
<li id="<%= dom_id(notification) %>" class="notification <%= "unread" if notification.unread? %>">
|
||||||
|
|
||||||
<% if notification.notifiable_available? %>
|
<% if notification.notifiable_available? %>
|
||||||
<%= link_to notification do %>
|
<%= link_to notification do %>
|
||||||
<p>
|
<p>
|
||||||
<em>
|
<em>
|
||||||
<%= t("notifications.index.#{notification.notifiable_action}",
|
<%= t("notifications.notification.action.#{notification.notifiable_action}",
|
||||||
count: notification.counter) %>
|
count: notification.counter) %>
|
||||||
</em>
|
</em>
|
||||||
<strong><%= notification.notifiable_title %></strong>
|
<strong id="<%= dom_id(notification) %>_title">
|
||||||
|
<%= notification.notifiable_title %>
|
||||||
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="time"><%= l notification.timestamp, format: :datetime %></p>
|
<p class="time">
|
||||||
|
<%= l notification.timestamp, format: :datetime %>
|
||||||
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p>
|
<p>
|
||||||
<strong>
|
<strong>
|
||||||
<%= t("notifications.index.notifiable_hidden") %>
|
<%= t("notifications.notification.notifiable_hidden") %>
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<% if notification.unread? %>
|
||||||
|
<%= link_to t("notifications.notification.mark_as_read"),
|
||||||
|
mark_as_read_notification_path(notification),
|
||||||
|
method: :put, remote: true,
|
||||||
|
class: "mark-notification small" %>
|
||||||
|
<% else %>
|
||||||
|
<%= link_to t("notifications.notification.mark_as_unread"),
|
||||||
|
mark_as_unread_notification_path(notification),
|
||||||
|
method: :put, remote: true,
|
||||||
|
class: "mark-notification small" %>
|
||||||
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
@@ -1,18 +1,28 @@
|
|||||||
<div class="row margin-bottom">
|
<div class="row margin-bottom">
|
||||||
<div class="small-12 column relative">
|
<div class="small-12 column">
|
||||||
|
|
||||||
<h1 class="float-left"><%= t("notifications.index.title") %></h1>
|
<h1 class="inline-block margin-bottom">
|
||||||
|
<%= t("notifications.index.title") %>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<%= link_to t("notifications.index.mark_all_as_read"),
|
||||||
|
mark_all_as_read_notifications_path, method: :put,
|
||||||
|
class: "button hollow float-right-medium" %>
|
||||||
|
|
||||||
|
<ul class="menu simple clear">
|
||||||
|
<li class="active">
|
||||||
|
<h2><%= link_to t("notifications.index.unread"), notifications_path %></h2>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<%= link_to t("notifications.index.read"), read_notifications_path %>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<% if @notifications.empty? %>
|
<% if @notifications.empty? %>
|
||||||
<div data-alert class="callout primary margin-top clear">
|
<div data-alert class="callout primary margin-top clear">
|
||||||
<%= t("notifications.index.empty_notifications") %>
|
<%= t("notifications.index.empty_notifications") %>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="float-right margin-top">
|
|
||||||
<%= link_to t("notifications.index.mark_all_as_read"),
|
|
||||||
mark_all_as_read_notifications_path, method: :put %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class="no-bullet clear notifications-list">
|
<ul class="no-bullet clear notifications-list">
|
||||||
<%= render @notifications %>
|
<%= render @notifications %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
1
app/views/notifications/mark_as_read.js.erb
Normal file
1
app/views/notifications/mark_as_read.js.erb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
$("#notification_<%= @notification.id %>").hide()
|
||||||
1
app/views/notifications/mark_as_unread.js.erb
Normal file
1
app/views/notifications/mark_as_unread.js.erb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
$("#notification_<%= @notification.id %>").hide()
|
||||||
19
app/views/notifications/read.html.erb
Normal file
19
app/views/notifications/read.html.erb
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<div class="row margin-bottom">
|
||||||
|
<div class="small-12 column">
|
||||||
|
|
||||||
|
<h1><%= t("notifications.index.title") %></h1>
|
||||||
|
|
||||||
|
<ul class="menu simple clear">
|
||||||
|
<li>
|
||||||
|
<%= link_to t("notifications.index.unread"), notifications_path %>
|
||||||
|
</li>
|
||||||
|
<li class="active">
|
||||||
|
<h2><%= link_to t("notifications.index.read"), read_notifications_path %></h2>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="no-bullet clear notifications-list">
|
||||||
|
<%= render @notifications %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -164,9 +164,7 @@ ignore_unused:
|
|||||||
- 'proposals.index.section_header.*'
|
- 'proposals.index.section_header.*'
|
||||||
- 'spending_proposals.index.search_form.*'
|
- 'spending_proposals.index.search_form.*'
|
||||||
- '*.index.search_form.*'
|
- '*.index.search_form.*'
|
||||||
- 'notifications.index.comments_on*'
|
- 'notifications.notification.action.*'
|
||||||
- 'notifications.index.replies_to*'
|
|
||||||
- 'notifications.index.proposal_notification*'
|
|
||||||
- 'legislation.processes.index.filter*'
|
- 'legislation.processes.index.filter*'
|
||||||
- 'legislation.processes.index.section_header.*'
|
- 'legislation.processes.index.section_header.*'
|
||||||
- 'helpers.page_entries_info.*' # kaminari
|
- 'helpers.page_entries_info.*' # kaminari
|
||||||
|
|||||||
@@ -234,11 +234,6 @@ en:
|
|||||||
help: Help
|
help: Help
|
||||||
my_account_link: My account
|
my_account_link: My account
|
||||||
my_activity_link: My activity
|
my_activity_link: My activity
|
||||||
notifications: Notifications
|
|
||||||
new_notifications:
|
|
||||||
one: You have a new notification
|
|
||||||
other: You have %{count} new notifications
|
|
||||||
no_notifications: "You don't have new notifications"
|
|
||||||
open: open
|
open: open
|
||||||
open_city_slogan_html: There are cities that are governed directly by their inhabitants, who <b>discuss</b> the topics they are concerned about, <b>propose</b> ideas to improve their lives and <b>decide</b> among themselves which ones will be carried out.
|
open_city_slogan_html: There are cities that are governed directly by their inhabitants, who <b>discuss</b> the topics they are concerned about, <b>propose</b> ideas to improve their lives and <b>decide</b> among themselves which ones will be carried out.
|
||||||
open_city_title: Love the city, and it will become a city you love
|
open_city_title: Love the city, and it will become a city you love
|
||||||
@@ -247,6 +242,12 @@ en:
|
|||||||
poll_questions: Voting
|
poll_questions: Voting
|
||||||
budgets: Participatory budgeting
|
budgets: Participatory budgeting
|
||||||
spending_proposals: Spending Proposals
|
spending_proposals: Spending Proposals
|
||||||
|
notification_item:
|
||||||
|
new_notifications:
|
||||||
|
one: You have a new notification
|
||||||
|
other: You have %{count} new notifications
|
||||||
|
notifications: Notifications
|
||||||
|
no_notifications: "You don't have new notifications"
|
||||||
admin:
|
admin:
|
||||||
watch_form_message: 'You have unsaved changes. Do you confirm to leave the page?'
|
watch_form_message: 'You have unsaved changes. Do you confirm to leave the page?'
|
||||||
legacy_legislation:
|
legacy_legislation:
|
||||||
@@ -259,19 +260,25 @@ en:
|
|||||||
locale: English
|
locale: English
|
||||||
notifications:
|
notifications:
|
||||||
index:
|
index:
|
||||||
|
empty_notifications: You don't have new notifications.
|
||||||
|
mark_all_as_read: Mark all as read
|
||||||
|
read: All notifications
|
||||||
|
title: Notifications
|
||||||
|
unread: Unread
|
||||||
|
notification:
|
||||||
|
action:
|
||||||
comments_on:
|
comments_on:
|
||||||
one: Someone commented on
|
one: Someone commented on
|
||||||
other: There are %{count} new comments on
|
other: There are %{count} new comments on
|
||||||
empty_notifications: You don't have new notifications.
|
|
||||||
notifiable_hidden: This resource is not available anymore.
|
|
||||||
mark_all_as_read: Mark all as read
|
|
||||||
proposal_notification:
|
proposal_notification:
|
||||||
one: There is one new notification on
|
one: There is one new notification on
|
||||||
other: There are %{count} new notifications on
|
other: There are %{count} new notifications on
|
||||||
replies_to:
|
replies_to:
|
||||||
one: Someone replied to your comment on
|
one: Someone replied to your comment on
|
||||||
other: There are %{count} new replies to your comment on
|
other: There are %{count} new replies to your comment on
|
||||||
title: Notifications
|
mark_as_read: Mark as read
|
||||||
|
mark_as_unread: Mark as unread
|
||||||
|
notifiable_hidden: This resource is not available anymore.
|
||||||
map:
|
map:
|
||||||
title: "Districts"
|
title: "Districts"
|
||||||
proposal_for_district: "Start a proposal for your district"
|
proposal_for_district: "Start a proposal for your district"
|
||||||
|
|||||||
@@ -234,11 +234,6 @@ es:
|
|||||||
help: Ayuda
|
help: Ayuda
|
||||||
my_account_link: Mi cuenta
|
my_account_link: Mi cuenta
|
||||||
my_activity_link: Mi actividad
|
my_activity_link: Mi actividad
|
||||||
notifications: Notificaciones
|
|
||||||
new_notifications:
|
|
||||||
one: Tienes una nueva notificación
|
|
||||||
other: Tienes %{count} notificaciones nuevas
|
|
||||||
no_notifications: "No tienes notificaciones nuevas"
|
|
||||||
open: abierto
|
open: abierto
|
||||||
open_city_slogan_html: Existen ciudades gobernadas directamente por sus habitantes, que <strong>debaten</strong> sobre temas que les preocupan, <strong>proponen</strong> ideas para mejorar sus vidas y <strong>deciden</strong> entre todas y todos las que se llevan a cabo.
|
open_city_slogan_html: Existen ciudades gobernadas directamente por sus habitantes, que <strong>debaten</strong> sobre temas que les preocupan, <strong>proponen</strong> ideas para mejorar sus vidas y <strong>deciden</strong> entre todas y todos las que se llevan a cabo.
|
||||||
open_city_title: La ciudad que quieres será la ciudad que quieras
|
open_city_title: La ciudad que quieres será la ciudad que quieras
|
||||||
@@ -247,6 +242,12 @@ es:
|
|||||||
poll_questions: Votaciones
|
poll_questions: Votaciones
|
||||||
budgets: Presupuestos participativos
|
budgets: Presupuestos participativos
|
||||||
spending_proposals: Propuestas de inversión
|
spending_proposals: Propuestas de inversión
|
||||||
|
notification_item:
|
||||||
|
new_notifications:
|
||||||
|
one: Tienes una nueva notificación
|
||||||
|
other: Tienes %{count} notificaciones nuevas
|
||||||
|
notifications: Notificaciones
|
||||||
|
no_notifications: "No tienes notificaciones nuevas"
|
||||||
admin:
|
admin:
|
||||||
watch_form_message: 'Has realizado cambios que no han sido guardados. ¿Seguro que quieres abandonar la página?'
|
watch_form_message: 'Has realizado cambios que no han sido guardados. ¿Seguro que quieres abandonar la página?'
|
||||||
legacy_legislation:
|
legacy_legislation:
|
||||||
@@ -259,19 +260,25 @@ es:
|
|||||||
locale: Español
|
locale: Español
|
||||||
notifications:
|
notifications:
|
||||||
index:
|
index:
|
||||||
|
empty_notifications: No tienes notificaciones nuevas.
|
||||||
|
mark_all_as_read: Marcar todas como leídas
|
||||||
|
read: Todas
|
||||||
|
title: Notificaciones
|
||||||
|
unread: Nuevas
|
||||||
|
notification:
|
||||||
|
action:
|
||||||
comments_on:
|
comments_on:
|
||||||
one: Hay un nuevo comentario en
|
one: Hay un nuevo comentario en
|
||||||
other: Hay %{count} comentarios nuevos en
|
other: Hay %{count} comentarios nuevos en
|
||||||
empty_notifications: No tienes notificaciones nuevas.
|
|
||||||
notifiable_hidden: Este elemento ya no está disponible.
|
|
||||||
mark_all_as_read: Marcar todas como leídas
|
|
||||||
proposal_notification:
|
proposal_notification:
|
||||||
one: Hay una nueva notificación en
|
one: Hay una nueva notificación en
|
||||||
other: Hay %{count} nuevas notificaciones en
|
other: Hay %{count} nuevas notificaciones en
|
||||||
replies_to:
|
replies_to:
|
||||||
one: Hay una respuesta nueva a tu comentario en
|
one: Hay una respuesta nueva a tu comentario en
|
||||||
other: Hay %{count} nuevas respuestas a tu comentario en
|
other: Hay %{count} nuevas respuestas a tu comentario en
|
||||||
title: Notificaciones
|
mark_as_read: Marcar como leída
|
||||||
|
mark_as_unread: Marcar como no leída
|
||||||
|
notifiable_hidden: Este elemento ya no está disponible.
|
||||||
map:
|
map:
|
||||||
title: "Distritos"
|
title: "Distritos"
|
||||||
proposal_for_district: "Crea una propuesta para tu distrito"
|
proposal_for_district: "Crea una propuesta para tu distrito"
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
resources :notifications, only: [:index, :show] do
|
resources :notifications, only: [:index, :show] do
|
||||||
|
put :mark_as_read, on: :member
|
||||||
put :mark_all_as_read, on: :collection
|
put :mark_all_as_read, on: :collection
|
||||||
|
put :mark_as_unread, on: :member
|
||||||
|
get :read, on: :collection
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :proposal_notifications, only: [:new, :create, :show]
|
resources :proposal_notifications, only: [:new, :create, :show]
|
||||||
|
|||||||
@@ -30,5 +30,6 @@ require_relative 'dev_seeds/polls'
|
|||||||
require_relative 'dev_seeds/communities'
|
require_relative 'dev_seeds/communities'
|
||||||
require_relative 'dev_seeds/legislation_processes'
|
require_relative 'dev_seeds/legislation_processes'
|
||||||
require_relative 'dev_seeds/newsletters'
|
require_relative 'dev_seeds/newsletters'
|
||||||
|
require_relative 'dev_seeds/notifications'
|
||||||
|
|
||||||
log "All dev seeds created successfuly 👍"
|
log "All dev seeds created successfuly 👍"
|
||||||
|
|||||||
16
db/dev_seeds/notifications.rb
Normal file
16
db/dev_seeds/notifications.rb
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
section "Creating comment notifications" do
|
||||||
|
User.all.each do |user|
|
||||||
|
debate = Debate.create!(author: user,
|
||||||
|
title: Faker::Lorem.sentence(3).truncate(60),
|
||||||
|
description: "<p>#{Faker::Lorem.paragraphs.join('</p><p>')}</p>",
|
||||||
|
tag_list: ActsAsTaggableOn::Tag.all.sample(3).join(','),
|
||||||
|
geozone: Geozone.reorder("RANDOM()").first,
|
||||||
|
terms_of_service: "1")
|
||||||
|
|
||||||
|
comment = Comment.create!(user: User.reorder("RANDOM()").first,
|
||||||
|
body: Faker::Lorem.sentence,
|
||||||
|
commentable: debate)
|
||||||
|
|
||||||
|
Notification.add(user, comment)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class AddReadAtToNotifications < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
add_column :notifications, :read_at, :timestamp
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -640,6 +640,7 @@ ActiveRecord::Schema.define(version: 20180320104823) do
|
|||||||
t.string "notifiable_type"
|
t.string "notifiable_type"
|
||||||
t.integer "counter", default: 1
|
t.integer "counter", default: 1
|
||||||
t.datetime "emailed_at"
|
t.datetime "emailed_at"
|
||||||
|
t.datetime "read_at"
|
||||||
end
|
end
|
||||||
|
|
||||||
add_index "notifications", ["user_id"], name: "index_notifications_on_user_id", using: :btree
|
add_index "notifications", ["user_id"], name: "index_notifications_on_user_id", using: :btree
|
||||||
|
|||||||
@@ -705,6 +705,10 @@ FactoryBot.define do
|
|||||||
factory :notification do
|
factory :notification do
|
||||||
user
|
user
|
||||||
association :notifiable, factory: :proposal
|
association :notifiable, factory: :proposal
|
||||||
|
|
||||||
|
trait :read do
|
||||||
|
read_at Time.current
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
factory :geozone do
|
factory :geozone do
|
||||||
|
|||||||
@@ -4,42 +4,128 @@ feature "Notifications" do
|
|||||||
|
|
||||||
let(:user) { create :user }
|
let(:user) { create :user }
|
||||||
|
|
||||||
context "mark as read" do
|
background do
|
||||||
|
login_as(user)
|
||||||
|
visit root_path
|
||||||
|
end
|
||||||
|
|
||||||
scenario "mark a single notification as read" do
|
scenario "View all" do
|
||||||
notification = create :notification, user: user
|
read1 = create(:notification, :read, user: user)
|
||||||
|
read2 = create(:notification, :read, user: user)
|
||||||
|
unread = create(:notification, user: user)
|
||||||
|
|
||||||
login_as user
|
click_notifications_icon
|
||||||
visit notifications_path
|
click_link "All notifications"
|
||||||
|
|
||||||
expect(page).to have_css ".notification", count: 1
|
expect(page).to have_css(".notification", count: 2)
|
||||||
|
expect(page).to have_content(read1.notifiable_title)
|
||||||
|
expect(page).to have_content(read2.notifiable_title)
|
||||||
|
expect(page).to_not have_content(unread.notifiable_title)
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario "View unread" do
|
||||||
|
unread1 = create(:notification, user: user)
|
||||||
|
unread2 = create(:notification, user: user)
|
||||||
|
read = create(:notification, :read, user: user)
|
||||||
|
|
||||||
|
click_notifications_icon
|
||||||
|
click_link "Unread"
|
||||||
|
|
||||||
|
expect(page).to have_css(".notification", count: 2)
|
||||||
|
expect(page).to have_content(unread1.notifiable_title)
|
||||||
|
expect(page).to have_content(unread2.notifiable_title)
|
||||||
|
expect(page).to_not have_content(read.notifiable_title)
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario "View single notification" do
|
||||||
|
proposal = create(:proposal)
|
||||||
|
notification = create(:notification, user: user, notifiable: proposal)
|
||||||
|
|
||||||
|
click_notifications_icon
|
||||||
|
|
||||||
first(".notification a").click
|
first(".notification a").click
|
||||||
visit notifications_path
|
expect(page).to have_current_path(proposal_path(proposal))
|
||||||
|
|
||||||
|
visit notifications_path
|
||||||
expect(page).to have_css ".notification", count: 0
|
expect(page).to have_css ".notification", count: 0
|
||||||
|
|
||||||
|
visit read_notifications_path
|
||||||
|
expect(page).to have_css ".notification", count: 1
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario "mark all notifications as read" do
|
scenario "Mark as read", :js do
|
||||||
2.times { create :notification, user: user }
|
notification1 = create(:notification, user: user)
|
||||||
|
notification2 = create(:notification, user: user)
|
||||||
|
|
||||||
login_as user
|
click_notifications_icon
|
||||||
visit notifications_path
|
|
||||||
|
|
||||||
expect(page).to have_css ".notification", count: 2
|
within("#notification_#{notification1.id}") do
|
||||||
|
click_link "Mark as read"
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_css(".notification", count: 1)
|
||||||
|
expect(page).to have_content(notification2.notifiable_title)
|
||||||
|
expect(page).to_not have_content(notification1.notifiable_title)
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario "Mark all as read" do
|
||||||
|
notification1 = create(:notification, user: user)
|
||||||
|
notification2 = create(:notification, user: user)
|
||||||
|
|
||||||
|
click_notifications_icon
|
||||||
|
|
||||||
|
expect(page).to have_css(".notification", count: 2)
|
||||||
click_link "Mark all as read"
|
click_link "Mark all as read"
|
||||||
|
|
||||||
expect(page).to have_css ".notification", count: 0
|
expect(page).to have_css(".notification", count: 0)
|
||||||
expect(page).to have_current_path(notifications_path)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
scenario "Mark as unread", :js do
|
||||||
|
notification1 = create(:notification, :read, user: user)
|
||||||
|
notification2 = create(:notification, user: user)
|
||||||
|
|
||||||
|
click_notifications_icon
|
||||||
|
click_link "All notifications"
|
||||||
|
|
||||||
|
expect(page).to have_css(".notification", count: 1)
|
||||||
|
within("#notification_#{notification1.id}") do
|
||||||
|
click_link "Mark as unread"
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario "no notifications" do
|
expect(page).to have_css(".notification", count: 0)
|
||||||
login_as user
|
|
||||||
visit notifications_path
|
visit notifications_path
|
||||||
|
expect(page).to have_css(".notification", count: 2)
|
||||||
|
expect(page).to have_content(notification1.notifiable_title)
|
||||||
|
expect(page).to have_content(notification2.notifiable_title)
|
||||||
|
end
|
||||||
|
|
||||||
expect(page).to have_content "You don't have new notifications"
|
scenario "Bell" do
|
||||||
|
create(:notification, user: user)
|
||||||
|
visit root_path
|
||||||
|
|
||||||
|
within("#notifications") do
|
||||||
|
expect(page).to have_css(".icon-circle")
|
||||||
|
end
|
||||||
|
|
||||||
|
click_notifications_icon
|
||||||
|
first(".notification a").click
|
||||||
|
|
||||||
|
within("#notifications") do
|
||||||
|
expect(page).to_not have_css(".icon-circle")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario "No notifications" do
|
||||||
|
click_notifications_icon
|
||||||
|
expect(page).to have_content "You don't have new notifications."
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario "User not logged in" do
|
||||||
|
logout
|
||||||
|
visit root_path
|
||||||
|
|
||||||
|
expect(page).to_not have_css("#notifications")
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,14 +2,48 @@ require 'rails_helper'
|
|||||||
|
|
||||||
describe Notification do
|
describe Notification do
|
||||||
|
|
||||||
describe "#unread (scope)" do
|
let(:notification) { build(:notification) }
|
||||||
it "returns only unread notifications" do
|
|
||||||
2.times { create :notification }
|
context "validations" do
|
||||||
expect(described_class.unread.size).to be 2
|
|
||||||
|
it "should be valid" do
|
||||||
|
expect(notification).to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should not be valid without a user" do
|
||||||
|
notification.user = nil
|
||||||
|
expect(notification).to_not be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
context "scopes" do
|
||||||
|
|
||||||
|
describe "#read" do
|
||||||
|
it "returns only read notifications" do
|
||||||
|
read_notification1 = create(:notification, :read)
|
||||||
|
read_notification2 = create(:notification, :read)
|
||||||
|
unread_notification = create(:notification)
|
||||||
|
|
||||||
|
expect(described_class.read).to include read_notification1
|
||||||
|
expect(described_class.read).to include read_notification2
|
||||||
|
expect(described_class.read).not_to include unread_notification
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#recent (scope)" do
|
describe "#unread" do
|
||||||
|
it "returns only unread notifications" do
|
||||||
|
read_notification = create(:notification, :read)
|
||||||
|
unread_notification1 = create(:notification)
|
||||||
|
unread_notification2 = create(:notification)
|
||||||
|
|
||||||
|
expect(described_class.unread).to include unread_notification1
|
||||||
|
expect(described_class.unread).to include unread_notification2
|
||||||
|
expect(described_class.unread).not_to include read_notification
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#recent" do
|
||||||
it "returns notifications sorted by id descendant" do
|
it "returns notifications sorted by id descendant" do
|
||||||
old_notification = create :notification
|
old_notification = create :notification
|
||||||
new_notification = create :notification
|
new_notification = create :notification
|
||||||
@@ -21,13 +55,39 @@ describe Notification do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#for_render (scope)" do
|
describe "#for_render" do
|
||||||
it "returns notifications including notifiable and user" do
|
it "returns notifications including notifiable and user" do
|
||||||
allow(described_class).to receive(:includes).with(:notifiable).exactly(:once)
|
allow(described_class).to receive(:includes).with(:notifiable).exactly(:once)
|
||||||
described_class.for_render
|
described_class.for_render
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#mark_as_read" do
|
||||||
|
it "destroys notification" do
|
||||||
|
notification = create(:notification)
|
||||||
|
expect(described_class.read.size).to eq 0
|
||||||
|
expect(described_class.unread.size).to eq 1
|
||||||
|
|
||||||
|
notification.mark_as_read
|
||||||
|
expect(described_class.read.size).to eq 1
|
||||||
|
expect(described_class.unread.size).to eq 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#mark_as_unread" do
|
||||||
|
it "destroys notification" do
|
||||||
|
notification = create(:notification, :read)
|
||||||
|
expect(described_class.unread.size).to eq 0
|
||||||
|
expect(described_class.read.size).to eq 1
|
||||||
|
|
||||||
|
notification.mark_as_unread
|
||||||
|
expect(described_class.unread.size).to eq 1
|
||||||
|
expect(described_class.read.size).to eq 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "#timestamp" do
|
describe "#timestamp" do
|
||||||
it "returns the timestamp of the trackable object" do
|
it "returns the timestamp of the trackable object" do
|
||||||
comment = create :comment
|
comment = create :comment
|
||||||
@@ -37,13 +97,66 @@ describe Notification do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#mark_as_read" do
|
describe "#existent" do
|
||||||
it "destroys notification" do
|
it "returns the notifiable when there is an existent notification of that notifiable" do
|
||||||
notification = create :notification
|
user = create(:user)
|
||||||
expect(described_class.unread.size).to eq 1
|
comment = create(:comment)
|
||||||
|
notification = create(:notification, user: user, notifiable: comment)
|
||||||
|
|
||||||
notification.mark_as_read
|
expect(described_class.existent(user, comment)).to eq(notification)
|
||||||
expect(described_class.unread.size).to eq 0
|
end
|
||||||
|
|
||||||
|
it "returns nil when there are no notifications of that notifiable for a user" do
|
||||||
|
user = create(:user)
|
||||||
|
comment1 = create(:comment)
|
||||||
|
comment2 = create(:comment)
|
||||||
|
create(:notification, user: user, notifiable: comment1)
|
||||||
|
|
||||||
|
expect(described_class.existent(user, comment2)).to eq(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nil when there are notifications of a notifiable for another user" do
|
||||||
|
user1 = create(:user)
|
||||||
|
user2 = create(:user)
|
||||||
|
comment = create(:comment)
|
||||||
|
notification = create(:notification, user: user1, notifiable: comment)
|
||||||
|
|
||||||
|
expect(described_class.existent(user2, comment)).to eq(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#add" do
|
||||||
|
it "creates a new notification" do
|
||||||
|
user = create(:user)
|
||||||
|
comment = create(:comment)
|
||||||
|
|
||||||
|
described_class.add(user, comment)
|
||||||
|
expect(user.notifications.count).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "increments the notification counter for an unread notification of the same notifiable" do
|
||||||
|
user = create(:user)
|
||||||
|
comment = create(:comment)
|
||||||
|
|
||||||
|
described_class.add(user, comment)
|
||||||
|
described_class.add(user, comment)
|
||||||
|
|
||||||
|
expect(user.notifications.count).to eq(1)
|
||||||
|
expect(user.notifications.first.counter).to eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates a new notification for a read notification of the same notifiable" do
|
||||||
|
user = create(:user)
|
||||||
|
comment = create(:comment)
|
||||||
|
|
||||||
|
first_notification = described_class.add(user, comment)
|
||||||
|
first_notification.update(read_at: Time.current)
|
||||||
|
|
||||||
|
second_notification = described_class.add(user, comment)
|
||||||
|
|
||||||
|
expect(user.notifications.count).to eq(2)
|
||||||
|
expect(first_notification.counter).to eq(1)
|
||||||
|
expect(second_notification.counter).to eq(1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -353,4 +353,7 @@ module CommonActions
|
|||||||
fill_in "newsletter_body", with: (options[:body] || "This is a different body")
|
fill_in "newsletter_body", with: (options[:body] || "This is a different body")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def click_notifications_icon
|
||||||
|
find("#notifications a").click
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user