Merge pull request #188 from biosistemika/global-notifications

Global notifications
This commit is contained in:
Luka Murn 2016-10-04 08:30:50 +02:00 committed by GitHub
commit 61cb9651c7
21 changed files with 449 additions and 21 deletions

View file

@ -1,12 +1,60 @@
/* Loading overlay for search */
$("#search-bar").submit(function (){
if( $("#update-canvas") ){
$(document.body).spin(true);
setTimeout(function(){
$(".spinner").remove();
}, 1000);
} else {
animateSpinner();
}
});
(function(){
'use strict';
/* Loading overlay for search */
$("#search-bar").submit(function (){
if( $("#update-canvas") ){
$(document.body).spin(true);
setTimeout(function(){
$(".spinner").remove();
}, 1000);
} else {
animateSpinner();
}
});
function loadDropdownNotifications() {
var button = $('#notifications-dropdown');
button
.on('click', function() {
$.ajax({
url: button.attr('data-href'),
type: 'GET',
dataType: 'json',
beforeSend: animateSpinner($('.notifications-dropdown-header'), true),
success: function(data) {
$('.notifications-dropdown-header')
.nextAll('li.notification')
.remove();
$('.notifications-dropdown-header')
.after(data.html);
animateSpinner($('.notifications-dropdown-header'), false);
}
});
$('#count-notifications').hide();
});
}
function loadUnseenNotificationsNumber() {
var notificationCount = $('#count-notifications');
$.ajax({
url: notificationCount.attr('data-href'),
type: 'GET',
dataType: 'json',
success: function(data) {
notificationCount.html('');
if ( data.notificationNmber > 0 ) {
notificationCount.html(data.notificationNmber);
notificationCount.show();
} else {
notificationCount.hide();
}
}
});
}
// init
loadDropdownNotifications();
loadUnseenNotificationsNumber();
})();

View file

@ -72,6 +72,93 @@ table {
max-width: 400px;
}
#notifications-dropdown {
.fa-bell {
font-size: 15px;
}
#count-notifications {
background-color: $color-theme-primary;
border-radius: 5px;
color: $color-wild-sand;
font-size: 12px;
font-weight: bold;
margin-left: 10px;
margin-top: -10px;
padding-left: 4px;
padding-right: 4px;
position: fixed;
z-index: 999999;
display: none;
}
}
@media(max-width:768px) {
#count-notifications {
position: absolute !important;
margin-left: -4px !important;
margin-top: 10px !important;
}
}
.dropdown-notifications {
max-height: 500px;
min-width: 450px;
overflow-x: hidden;
overflow-y: scroll;
padding-bottom: 0;
padding-top: 0;
.notification {
border-bottom: 1px solid $color-alto;
padding-bottom: 10px;
padding-top: 10px;
&:hover {
background-color: $color-concrete;
}
}
.unseen {
border-left: 4px solid $color-theme-primary;
}
.text-center {
margin-left: 10px;
padding-top: 10px;
}
.assignment {
background-color: $color-theme-primary;
border-radius: 50%;
color: $color-wild-sand;
font-size: 15px;
padding: 7px;
}
.system_message {
background-color: $color-theme-secondary;
border-radius: 50%;
color: $color-wild-sand;
font-size: 13px;
padding: 7px 10px;
}
.notifications-dropdown-header {
background-color: $color-theme-primary;
color: $color-wild-sand;
font-weight: bold;
padding: 8px;
}
.notifications-dropdown-footer {
background-color: $color-mystic;
padding: 8px;
text-align: center;
}
}
#search-menu {
padding-right: 0;

View file

@ -0,0 +1,28 @@
class UserNotificationsController < ApplicationController
def recent_notifications
@recent_notifications = UserNotification.recent_notifications(current_user)
respond_to do |format|
format.json do
render json: {
html: render_to_string(
partial: 'recent_notifications.html.erb'
)
}
end
end
UserNotification.seen_by_user(current_user)
end
def unseen_notification
@number = UserNotification.unseen_notification_count(current_user)
respond_to do |format|
format.json do
render json: {
notificationNmber: @number
}
end
end
end
end

View file

@ -211,15 +211,23 @@ class Users::SettingsController < ApplicationController
user: @new_user_org.user,
organization: @new_user_org.organization
).exists? && @new_user_org.save
AppMailer.delay.invitation_to_organization(@new_user_org.user, @user_organization.user, @new_user_org.organization)
AppMailer.delay.invitation_to_organization(@new_user_org.user,
@user_organization.user,
@new_user_org.organization)
generate_notification(@user_organization.user,
@new_user_org.user,
@new_user_org.role_str,
@new_user_org.organization)
flash[:notice] = I18n.t(
"users.settings.organizations.edit.modal_add_user.existing_flash_success",
'users.settings.organizations.edit.modal_add_user.existing_flash_success',
user: @new_user_org.user.full_name,
role: @new_user_org.role_str
)
else
flash[:alert] =
I18n.t("users.settings.organizations.edit.modal_add_user.existing_flash_error")
I18n.t('users.settings.organizations.edit.modal_add_user.existing_flash_error')
end
# Either way, redirect back to organization page
@ -520,4 +528,21 @@ class Users::SettingsController < ApplicationController
)
end
def generate_notification(user, target_user, role, org)
title = I18n.t('activities.assign_user_to_organization',
assigned_user: target_user.name,
role: role,
organization: org.name,
assigned_by_user: user.name)
message = "#{I18n.t('search.index.organization')} #{org.name}"
notification = Notification.create(
type_of: :assignment,
title:
ActionController::Base.helpers.sanitize(title),
message:
ActionController::Base.helpers.sanitize(message),
)
UserNotification.create(notification: notification, user: target_user)
end
end

View file

@ -0,0 +1,16 @@
module NotificationsHelper
def create_system_notification(title, message)
notification = Notification.new
notification.title = title
notification.message = message
notification.type_of = :system_message
notification.transaction do
User.where.not(confirmed_at: nil).find_each do |u|
UserNotification
.new(user: u, notification: notification, checked: false)
.save!
end
notification.save!
end
end
end

View file

@ -1,4 +1,6 @@
class Activity < ActiveRecord::Base
after_create :generate_notification
enum type_of: [
:create_project,
:rename_project,
@ -48,4 +50,32 @@ class Activity < ActiveRecord::Base
belongs_to :project, inverse_of: :activities
belongs_to :my_module, inverse_of: :activities
belongs_to :user, inverse_of: :activities
private
def generate_notification
if %w(assign_user_to_project assign_user_to_module).include? type_of
notification_type = :assignment
else
notification_type = :recent_changes
end
task_m = "| #{I18n.t('search.index.module')} #{my_module.name}" if my_module
notification = Notification.create(
type_of: notification_type,
title:
ActionController::Base.helpers.sanitize(message, tags: %w(strong a)),
message:
ActionController::Base
.helpers.sanitize(
"#{I18n.t('search.index.project')} #{project.name} #{task_m}"
),
generator_user_id: user.id
)
project.users.each do |project_user|
next if project_user == user
UserNotification.create(notification: notification, user: project_user)
end
end
end

View file

@ -0,0 +1,13 @@
class Notification < ActiveRecord::Base
has_many :user_notifications, inverse_of: :notification
has_many :users, through: :user_notifications
belongs_to :generator_user, class_name: 'User'
enum type_of: [:assignment, :recent_changes, :system_message]
def already_seen(user)
UserNotification.where(notification: self, user: user)
.pluck(:checked)
.first
end
end

View file

@ -84,7 +84,8 @@ class User < ActiveRecord::Base
has_many :added_protocols, class_name: 'Protocol', foreign_key: 'added_by_id', inverse_of: :added_by
has_many :archived_protocols, class_name: 'Protocol', foreign_key: 'archived_by_id', inverse_of: :archived_by
has_many :restored_protocols, class_name: 'Protocol', foreign_key: 'restored_by_id', inverse_of: :restored_by
has_many :user_notifications, inverse_of: :user
has_many :notifications, through: :user_notifications
# If other errors besides parameter "avatar" exist,
# they will propagate to "avatar" also, so remove them
# and put all other (more specific ones) in it
@ -260,4 +261,3 @@ class User < ActiveRecord::Base
end
end
end

View file

@ -0,0 +1,22 @@
class UserNotification < ActiveRecord::Base
belongs_to :user
belongs_to :notification
def self.recent_notifications(user)
Notification.joins(:user_notifications)
.where('user_notifications.user_id = ?', user.id)
.order(created_at: :DESC)
.limit(10)
end
def self.unseen_notification_count(user)
where('user_id = ? AND checked = false', user.id).count
end
def self.seen_by_user(user)
where(user: user).map do |n|
n.checked = true
n.save
end
end
end

View file

@ -54,8 +54,26 @@
</ul>
<div>
<!-- help -->
<!-- profile info -->
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a href="#"
id="notifications-dropdown"
class="dropdown-toggle"
data-toggle="dropdown"
role="button"
aria-haspopup="true"
aria-expanded="false"
data-href="<%= recent_notifications_url(current_user) %>">
<%= fa_icon 'bell'%>
<span id="count-notifications"
data-href="<%= unseen_notification_url(current_user) %>"></span>
</a>
<ul class="dropdown-menu dropdown-notifications">
<li class="notifications-dropdown-header"><span><%= t('notifications.title') %></span><span class="pull-right"><%= t('nav.user.settings') %></span></li>
<li class="notifications-dropdown-footer">link to method goes here</li>
</ul>
</li>
<li class="dropdown">
<a href="#" id="help-link" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-info-sign"></span>
@ -69,10 +87,8 @@
<li><%= link_to t('nav.help.contact'), CONTACT_URL, target: "_blank" %></li>
</ul>
</li>
</ul>
<!-- profile info -->
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span><%= t('nav.user_greeting', full_name: current_user.full_name) %></span>

View file

@ -0,0 +1,28 @@
<% @recent_notifications.each do |notification| %>
<li class="notification <%= 'unseen' unless notification.already_seen(current_user) %>">
<div class="row">
<div class="col-xs-2">
<% if notification.type_of == 'recent_changes' %>
<div class="text-center">
<%= image_tag avatar_path(notification.generator_user, :icon_small), class: 'avatar' %>
</div>
<% end %>
<% if notification.type_of == 'assignment' %>
<div class="text-center">
<span class="assignment"><%= fa_icon 'newspaper-o' %></span>
</div>
<% end %>
<% if notification.type_of == 'system_message' %>
<div class="text-center">
<span class="system_message"><i class="glyphicon glyphicon-tower" aria-hidden="true"></i></span>
</div>
<% end %>
</div>
<div class="col-xs-10">
<strong><%= notification.title.html_safe %></strong> <br>
<%= l(notification.created_at, format: :full) %> | <%= notification.message %>
</div>
</div>
<li>
<% end %>

View file

@ -1057,6 +1057,7 @@ en:
delete_step_comment: "<i>%{user}</i> deleted comment on Step %{step} <strong>%{step_name}</strong>."
edit_result_comment: "<i>%{user}</i> edited comment on result <strong>%{result}</strong>."
delete_result_comment: "<i>%{user}</i> deleted comment on result <strong>%{result}</strong>."
assign_user_to_organization: "<i>%{assigned_user}</i> was added as %{role} to organization <strong>%{organization}</strong> by <i>%{assigned_by_user}</i>."
user_my_modules:
new:
@ -1508,6 +1509,8 @@ en:
sentence_seven_html: "<strong>Info tab</strong> <br>In the upper right corner of the Dashboard you can now access the Info button, where you can find links to support on how to use sciNote features (e.g., Tutorials, FAQ) or how to change your current sciNote Plan (e.g., Professional support and customization, Plans). In case you have some other questions or suggestions, click on the Contact us link."
confim: "OK, got it"
notifications:
title: "Notifications"
# This section contains general words that can be used in any parts of
# application.

View file

@ -32,6 +32,17 @@ Rails.application.routes.draw do
get "users/settings/user_organizations/:user_organization_id/destroy_html", to: "users/settings#destroy_user_organization_html", as: "destroy_user_organization_html"
delete "users/settings/user_organizations/:user_organization_id", to: "users/settings#destroy_user_organization", as: "destroy_user_organization"
# Notifications
get 'users/:id/recent_notifications',
to: 'user_notifications#recent_notifications',
as: 'recent_notifications',
defaults: { format: 'json' }
get 'users/:id/unseen_notification',
to: 'user_notifications#unseen_notification',
as: 'unseen_notification',
defaults: { format: 'json' }
resources :organizations, only: [] do
resources :samples, only: [:new, :create]
resources :sample_types, only: [:new, :create]

View file

@ -0,0 +1,14 @@
class CreateNotifications < ActiveRecord::Migration
def change
create_table :notifications do |t|
t.string :title
t.string :message
t.integer :type_of, null: false
t.integer :generator_user_id
t.timestamps null: false
end
add_index :notifications, :created_at
add_foreign_key :notifications, :users, column: :generator_user_id
end
end

View file

@ -0,0 +1,11 @@
class CreateUserNotifications < ActiveRecord::Migration
def change
create_table :user_notifications do |t|
t.belongs_to :user, index: true, foreign_key: true
t.belongs_to :notification, index: true, foreign_key: true
t.boolean :checked, default: false
t.timestamps null: false
end
end
end

View file

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160809074757) do
ActiveRecord::Schema.define(version: 20160928114915) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -226,6 +226,17 @@ ActiveRecord::Schema.define(version: 20160809074757) do
add_index "my_modules", ["name"], name: "index_my_modules_on_name", using: :gist
add_index "my_modules", ["restored_by_id"], name: "index_my_modules_on_restored_by_id", using: :btree
create_table "notifications", force: :cascade do |t|
t.string "title"
t.string "message"
t.integer "type_of", null: false
t.integer "generator_user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "notifications", ["created_at"], name: "index_notifications_on_created_at", using: :btree
create_table "organizations", force: :cascade do |t|
t.string "name", null: false
t.datetime "created_at", null: false
@ -578,6 +589,17 @@ ActiveRecord::Schema.define(version: 20160809074757) do
add_index "user_my_modules", ["my_module_id"], name: "index_user_my_modules_on_my_module_id", using: :btree
add_index "user_my_modules", ["user_id"], name: "index_user_my_modules_on_user_id", using: :btree
create_table "user_notifications", force: :cascade do |t|
t.integer "user_id"
t.integer "notification_id"
t.boolean "checked", default: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "user_notifications", ["notification_id"], name: "index_user_notifications_on_notification_id", using: :btree
add_index "user_notifications", ["user_id"], name: "index_user_notifications_on_user_id", using: :btree
create_table "user_organizations", force: :cascade do |t|
t.integer "role", default: 1, null: false
t.integer "user_id", null: false
@ -681,6 +703,7 @@ ActiveRecord::Schema.define(version: 20160809074757) do
add_foreign_key "my_modules", "users", column: "created_by_id"
add_foreign_key "my_modules", "users", column: "last_modified_by_id"
add_foreign_key "my_modules", "users", column: "restored_by_id"
add_foreign_key "notifications", "users", column: "generator_user_id"
add_foreign_key "organizations", "users", column: "created_by_id"
add_foreign_key "organizations", "users", column: "last_modified_by_id"
add_foreign_key "project_comments", "comments"
@ -758,6 +781,8 @@ ActiveRecord::Schema.define(version: 20160809074757) do
add_foreign_key "user_my_modules", "my_modules"
add_foreign_key "user_my_modules", "users"
add_foreign_key "user_my_modules", "users", column: "assigned_by_id"
add_foreign_key "user_notifications", "notifications"
add_foreign_key "user_notifications", "users"
add_foreign_key "user_organizations", "organizations"
add_foreign_key "user_organizations", "users"
add_foreign_key "user_organizations", "users", column: "assigned_by_id"

View file

@ -0,0 +1,10 @@
namespace :notifications do
desc 'Creates new system notification for all active users'
task :new_release => :environment do
include NotificationsHelper
puts 'Creation of system notification for all active users with link to release notes'
create_system_notification('New release', 'http://scinote.net/docs/release-notes/')
end
end

11
test/fixtures/notifications.yml vendored Normal file
View file

@ -0,0 +1,11 @@
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
title: MyString
message: MyString
type_of: 1
two:
title: MyString
message: MyString
type_of: 1

11
test/fixtures/user_notifications.yml vendored Normal file
View file

@ -0,0 +1,11 @@
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
user_id:
notification_id:
checked: false
two:
user_id:
notification_id:
checked: false

View file

@ -0,0 +1,12 @@
require 'test_helper'
class NotificationTest < ActiveSupport::TestCase
should have_db_column(:title).of_type(:string)
should have_db_column(:message).of_type(:text)
should have_db_column(:type_of).of_type(:integer)
should have_db_column(:created_at).of_type(:datetime)
should have_db_column(:updated_at).of_type(:datetime)
should have_many(:user_notifications)
should have_many(:users)
end

View file

@ -0,0 +1,7 @@
require 'test_helper'
class UserNotificationTest < ActiveSupport::TestCase
should have_db_column(:checked).of_type(:boolean)
should belong_to(:user)
should belong_to(:notification)
end