├── Gemfile ├── Gemfile.lock ├── README.md ├── app ├── controllers │ └── app_notifications_controller.rb ├── helpers │ └── app_notifications_helper.rb ├── models │ └── app_notification.rb └── views │ ├── app_notifications │ ├── _ajax.html.erb │ ├── _layouts_base_html_head.html.erb │ ├── _my_account_preferences.html.erb │ └── index.html.erb │ ├── issues │ ├── _issue_add.html.erb │ ├── _issue_ajax_add.html.erb │ ├── _issue_ajax_edit.html.erb │ └── _issue_edit.html.erb │ └── settings │ └── _app_notifications_settings.html.erb ├── assets ├── javascripts │ └── app_notifications.js └── stylesheets │ └── app_notifications.css ├── config ├── locales │ ├── cs.yml │ ├── en.yml │ ├── es.yml │ ├── fr.yml │ ├── ru.yml │ └── tr.yml └── routes.rb ├── db └── migrate │ ├── 001_create_app_notifications.rb │ ├── 002_update_users.rb │ └── 003_update_users2.rb ├── faye.ru ├── faye_for_redmine ├── init.rb ├── lib ├── app_notifications_account_patch.rb ├── app_notifications_hook_listener.rb ├── app_notifications_issues_patch.rb └── app_notifications_journals_patch.rb └── test ├── fixtures └── app_notifications.yml ├── functional └── app_notifications_controller_test.rb ├── test_helper.rb └── unit └── app_notification_test.rb /Gemfile: -------------------------------------------------------------------------------- 1 | gem 'faye' 2 | 3 | group :test do 4 | gem "factory_girl", "~> 4.0" 5 | end -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | specs: 3 | addressable (2.3.6) 4 | cookiejar (0.3.2) 5 | em-http-request (1.1.2) 6 | addressable (>= 2.3.4) 7 | cookiejar 8 | em-socksify (>= 0.3) 9 | eventmachine (>= 1.0.3) 10 | http_parser.rb (>= 0.6.0) 11 | em-socksify (0.3.0) 12 | eventmachine (>= 1.0.0.beta.4) 13 | eventmachine (1.0.4) 14 | faye (1.1.0) 15 | cookiejar (>= 0.3.0) 16 | em-http-request (>= 0.3.0) 17 | eventmachine (>= 0.12.0) 18 | faye-websocket (>= 0.9.1) 19 | multi_json (>= 1.0.0) 20 | rack (>= 1.0.0) 21 | websocket-driver (>= 0.5.1) 22 | faye-websocket (0.9.2) 23 | eventmachine (>= 0.12.0) 24 | websocket-driver (>= 0.5.1) 25 | http_parser.rb (0.6.0) 26 | multi_json (1.10.1) 27 | rack (1.5.2) 28 | websocket-driver (0.5.1) 29 | websocket-extensions (>= 0.1.0) 30 | websocket-extensions (0.1.1) 31 | 32 | PLATFORMS 33 | ruby 34 | 35 | DEPENDENCIES 36 | faye 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redmine App Notifications 2 | 3 | App notifications plugin provides simple in application notifications for Redmine. It can replace default e-mail notifications. 4 | 5 | ## Installation and Setup 6 | 7 | 1. Follow the Redmine plugin installation steps at: http://www.redmine.org/wiki/redmine/Plugins 8 | 2. Run the plugin migrations `rake redmine:plugins:migrate RAILS_ENV=production` 9 | 3. Optional if you want to use Faye for server to client notifications 10 | 1. Install the Thin server `gem install thin` or configure faye.ru accordingly to the server you use. See https://github.com/faye/faye-websocket-ruby#running-your-socket-application for more details. 11 | 2. Copy the `faye_for_redmine` file to `/etc/init.d` 12 | 3. Modify `/etc/init.d/faye_for_redmine` to at least fill the right value for `IN_APP_NOTIFICATION_ROOT_PATH` 13 | 4. Makes the script starts at boot `cd /etc/init.d && update-rc.d faye_for_redmine defaults` 14 | 2. Start the Faye server `/etc/init.d/faye_for_redmine start` (you can stop it with `stop` instead of `start`). 15 | 3. In Administration > Plugins > Configure, modify `ip_address_or_name_of_your_server` to match your server IP address or name 16 | 4. Restart your Redmine web server 17 | 5. Login and configure the plugin (Administration > Plugins > Configure) 18 | 6. Enable In App Notifications in user account settings -> preferences 19 | -------------------------------------------------------------------------------- /app/controllers/app_notifications_controller.rb: -------------------------------------------------------------------------------- 1 | class AppNotificationsController < ApplicationController 2 | unloadable 3 | # helper :app_notifications 4 | # include AppNotificationsHelper 5 | 6 | def index 7 | @app_notifications = AppNotification.includes(:issue, :author, :journal).where(recipient_id: User.current.id).order("created_on desc") 8 | if request.xhr? 9 | @app_notifications = @app_notifications.limit(5) 10 | render :partial => "ajax" 11 | end 12 | 13 | if !params.has_key?(:viewed) && !params.has_key?(:new) && !params.has_key?(:commit) 14 | @viewed = false 15 | @new = true 16 | else 17 | params.has_key?(:viewed) ? @viewed = params['viewed'] : @viewed = false 18 | params.has_key?(:new) ? @new = params['new'] : @new = false 19 | end 20 | 21 | if(!@viewed && !@new) 22 | return @app_notifications = [] 23 | end 24 | if(@viewed != @new) 25 | @app_notifications = @app_notifications.where(viewed: true) if @viewed 26 | @app_notifications = @app_notifications.where(viewed: false) if @new 27 | end 28 | @limit = 10 29 | @app_notifications_pages = Paginator.new @app_notifications.count, @limit, params['page'] 30 | @offset ||= @app_notifications_pages.offset 31 | @app_notifications = @app_notifications.limit(@limit).offset(@offset) 32 | end 33 | 34 | def view 35 | @notification = AppNotification.find(params[:id]) 36 | if @notification.recipient == User.current 37 | AppNotification.update(@notification, :viewed => true) 38 | if request.xhr? 39 | if @notification.is_edited? 40 | render :partial => 'issues/issue_edit', :formats => [:html], :locals => { :notification => @notification, :journal => @notification.journal } 41 | else 42 | render :partial => 'issues/issue_add', :formats => [:html], :locals => { :notification => @notification } 43 | end 44 | else 45 | redirect_to :controller => 'issues', :action => 'show', :id => params[:issue_id], :anchor => params[:anchor] 46 | end 47 | end 48 | end 49 | 50 | def view_all 51 | AppNotification.where(:recipient_id => User.current.id, :viewed => false).update_all( :viewed => true ) 52 | redirect_to :action => 'index' 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /app/helpers/app_notifications_helper.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../../../app/helpers/issues_helper' 2 | 3 | module AppNotificationsHelper 4 | include IssuesHelper 5 | end 6 | -------------------------------------------------------------------------------- /app/models/app_notification.rb: -------------------------------------------------------------------------------- 1 | include GravatarHelper::PublicMethods 2 | include ERB::Util 3 | 4 | class AppNotification < ActiveRecord::Base 5 | belongs_to :recipient, :class_name => 'User', :foreign_key => 'recipient_id' 6 | belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' 7 | belongs_to :issue 8 | belongs_to :journal 9 | 10 | def deliver 11 | unless Setting.plugin_redmine_app_notifications['faye_server_adress'].empty? 12 | channel = "/notifications/private/#{recipient.id}" 13 | message = {:channel => channel, :data => { count: AppNotification.where(recipient_id: recipient.id, viewed: false).count, message: message_text, id: id, avatar: gravatar_url(author.mail, { :default => Setting.gravatar_default })}} 14 | uri = URI.parse(Setting.plugin_redmine_app_notifications['faye_server_adress']) 15 | Net::HTTP.post_form(uri, :message => message.to_json) 16 | end 17 | end 18 | 19 | def is_edited? 20 | journal.present? 21 | end 22 | 23 | def message_text 24 | if is_edited? 25 | I18n.t(:text_issue_updated, :id => "##{issue.id}", :author => author) 26 | else 27 | I18n.t(:text_issue_added, :id => "##{issue.id}", :author => author) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/views/app_notifications/_ajax.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
<%= l(:notifications) %>
3 |
4 | <% @app_notifications.where(viewed: false).each do |notification| %> 5 | <% if notification.is_edited? %> 6 | <%= render :partial => 'issues/issue_ajax_edit', :formats => [:html], :locals => { :notification => notification, :journal => notification.journal, :journal_details => details_to_strings(notification.journal.visible_details(notification.author), false, :only_path => false) }%> 7 | <% else %> 8 | <%= render :partial => 'issues/issue_ajax_add', :formats => [:html], :locals => { :notification => notification }%> 9 | <% end %> 10 | <% end %> 11 |
12 |
13 | "index"} )%>"><%= l(:see_all) %> 14 |
15 |
16 | -------------------------------------------------------------------------------- /app/views/app_notifications/_layouts_base_html_head.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :header_tags do %> 2 | <%= javascript_include_tag "app_notifications", :plugin => "redmine_app_notifications" %> 3 | <%= stylesheet_link_tag "app_notifications", :plugin => "redmine_app_notifications" %> 4 | <% unless Setting.plugin_redmine_app_notifications['faye_server_adress'].empty? %> 5 | <%= javascript_include_tag "#{Setting.plugin_redmine_app_notifications['faye_server_adress']}/faye.js" %> 6 | 61 | <% end %> 62 | <% end %> 63 | -------------------------------------------------------------------------------- /app/views/app_notifications/_my_account_preferences.html.erb: -------------------------------------------------------------------------------- 1 | <%= labelled_fields_for :user, @user do |user_fields| %> 2 |

<%= user_fields.check_box :app_notification %>

3 |

<%= user_fields.check_box :app_notification_desktop %>

4 | <% end %> 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/views/app_notifications/index.html.erb: -------------------------------------------------------------------------------- 1 |

<%= l(:notifications) %>

2 | 3 | <%= form_tag(url_for( {:controller => "app_notifications", :action => "index"} ), method: "get") do %> 4 | <%= check_box_tag(:viewed, 1, @viewed) %> 5 | <%= label_tag(:viewed, l(:viewed_notification)) %> 6 | <%= check_box_tag(:new, 1, @new) %> 7 | <%= label_tag(:new, l(:new_notification)) %> 8 | <%= submit_tag(l(:filter_button)) %> 9 | <% end %> 10 |
11 | 12 | <% @app_notifications.each do |notification| %> 13 |
"> 14 | <% if notification.is_edited? %> 15 | <%= render :partial => 'issues/issue_edit', :formats => [:html], :locals => { :notification => notification, :journal => notification.journal, :journal_details => details_to_strings(notification.journal.visible_details(notification.author), false, :only_path => false) }%> 16 | <% else %> 17 | <%= render :partial => 'issues/issue_add', :formats => [:html], :locals => { :notification => notification }%> 18 | <% end %> 19 |
20 | <%= format_time(notification.created_on) %> 21 |
22 |
23 |
24 | <% end %> 25 | <% unless @app_notifications.empty? %> 26 |

<%= pagination_links_full @app_notifications_pages, @app_notifications_count %>

27 | <% end %> 28 | 29 | <%= button_to l(:mark_all_as_seen), action: "view_all" %> 30 | -------------------------------------------------------------------------------- /app/views/issues/_issue_add.html.erb: -------------------------------------------------------------------------------- 1 | <% unless notification.issue.nil? %> 2 | <%if !notification.viewed %> 3 | 4 | <%= l(:mark_as_seen) %> 5 | 6 | <% end %> 7 |

8 | <%= avatar(notification.issue.author) %> 9 | 10 | <%= l(:text_issue_added, :id => "##{notification.issue.id}", :author => h(notification.issue.author)) %> 11 | 12 |

13 | <% end %> 14 | -------------------------------------------------------------------------------- /app/views/issues/_issue_ajax_add.html.erb: -------------------------------------------------------------------------------- 1 | <% unless notification.issue.nil? %> 2 | 3 |
4 |
5 | <%= avatar(notification.issue.author, :size => "24") %> 6 | <%= l(:text_issue_added, :id => "##{notification.issue.id}", :author => h(notification.issue.author)) %> 7 |
8 |
<%= format_time(notification.created_on) %>
9 |
10 |
11 | <% end %> 12 | -------------------------------------------------------------------------------- /app/views/issues/_issue_ajax_edit.html.erb: -------------------------------------------------------------------------------- 1 | <% unless notification.issue.nil? %> 2 | "> 3 |
4 |
5 | <%= avatar(notification.journal.user, :size => "24") %> 6 | <%= l(:text_issue_updated, :id => "##{notification.issue.id}", :author => h(notification.journal.user)) %> 7 |
8 |
9 | <% journal_details.each do |string| %> 10 |
<%= string %>
11 | <% end %> 12 |
13 |
<%= format_time(notification.created_on) %>
14 |
15 |
16 | <% end %> 17 | -------------------------------------------------------------------------------- /app/views/issues/_issue_edit.html.erb: -------------------------------------------------------------------------------- 1 | <% unless notification.issue.nil? %> 2 | <%if !notification.viewed %> 3 | "> 4 | <%= l(:mark_as_seen) %> 5 | 6 | <% end %> 7 |

8 | <%= avatar(journal.user) %> 9 | "> 10 | <%= l(:text_issue_updated, :id => "##{notification.issue.id}", :author => h(journal.user)) %> 11 | 12 |

13 | 14 | <% if journal.private_notes? %> 15 | (<%= l(:field_private_notes) %>) 16 | <% end %> 17 | 18 | 23 | 24 | <%= textilizable(journal, :notes, :only_path => false) %> 25 | <% end %> 26 | -------------------------------------------------------------------------------- /app/views/settings/_app_notifications_settings.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% ["issue_added", "issue_updated", "issue_note_added", "issue_status_updated", "issue_assigned_to_updated", "issue_priority_updated"].each do |str| %> 5 | 6 | 10 | 11 | <% end %> 12 | 13 |
<%= l(:notifications_events_settings) %>
7 | > 8 | <%= l(str.to_sym) %> 9 |
14 | 15 | 16 | 17 | 18 | 21 | 22 |
<%= l(:faye_server_adress) %> 19 | 20 |
-------------------------------------------------------------------------------- /assets/javascripts/app_notifications.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() 2 | { 3 | $("#notificationsLink").click(function() 4 | { 5 | $("#notificationsContainer").remove(); 6 | 7 | $.ajax({ 8 | type: "GET", 9 | url: $(this).attr("href"), 10 | dataType: 'html', 11 | success: function(data) { 12 | $("#notificationsLink").parent().addClass('notification_li'); 13 | $("#notificationsLink").parent().append(data); 14 | $("#notificationsContainer").fadeToggle(300); 15 | } 16 | }); 17 | 18 | return false; 19 | }); 20 | 21 | //Document Click 22 | $(document).click(function() 23 | { 24 | $("#notificationsContainer").hide(); 25 | }); 26 | //Popup Click 27 | $("#notificationsContainer").click(function() 28 | { 29 | return false; 30 | }); 31 | 32 | $(".view-notification").click(function() 33 | { 34 | var link = $( this ); 35 | $.ajax({ 36 | type: "GET", 37 | url: $(this).attr("href"), 38 | dataType: 'html', 39 | success: function() { 40 | link.parent().removeClass( "new" ); 41 | link.remove(); 42 | } 43 | }); 44 | return false; 45 | }); 46 | 47 | var countText = $("#notification_count").text(); 48 | $("#notification_count").replaceWith("" + countText + ""); 49 | }); 50 | -------------------------------------------------------------------------------- /assets/stylesheets/app_notifications.css: -------------------------------------------------------------------------------- 1 | #notificationsFooter a{color:#333333;text-decoration:none} 2 | #notificationsFooter a:hover{color:#006699;text-decoration:none} 3 | #notificationsContainer { 4 | color: #121212; 5 | background-color: #fff; 6 | border: 1px solid rgba(100, 100, 100, .4); 7 | -webkit-box-shadow: 0 3px 8px rgba(0, 0, 0, .25); 8 | 9 | position: absolute; 10 | top: 30px; 11 | margin-left: -170px; 12 | width: 400px; 13 | z-index: 1000; 14 | display: none; 15 | font-size: 12px; 16 | } 17 | .theme-Purplemine2 #notificationsContainer { 18 | margin-left: 125px; 19 | } 20 | #notificationsContainer:before { 21 | content: ''; 22 | display: block; 23 | position: absolute; 24 | width: 0; 25 | height: 0; 26 | color: transparent; 27 | border: 10px solid black; 28 | border-color: transparent transparent white; 29 | margin-top: -20px; 30 | margin-left: 188px; 31 | } 32 | #notificationsTitle { 33 | z-index: 1000; 34 | font-weight: bold; 35 | padding: 8px; 36 | font-size: 13px; 37 | background-color: #ffffff; 38 | width: 384px; 39 | border-bottom: 1px solid #dddddd; 40 | } 41 | #notificationsFooter { 42 | background-color: #F1F1FF; 43 | text-align: center; 44 | font-weight: bold; 45 | padding: 8px; 46 | font-size: 13px; 47 | border-top: 1px solid #dddddd; 48 | } 49 | #notification_count { 50 | background: none repeat scroll 0 0 #cc0000; 51 | border-radius: 9px; 52 | color: #ffffff; 53 | font-size: 11px; 54 | font-weight: bold; 55 | margin-left: -5px; 56 | margin-top: -1px; 57 | padding: 1px 5px; 58 | position: absolute; 59 | } 60 | .notification-container { 61 | padding: 10px 15px; 62 | background-color: #FCFCFF 63 | } 64 | .notification-container:hover { 65 | background-color: #FFFFFF; 66 | } 67 | .notification-container.new { 68 | background-color: #F0F0FF; 69 | } 70 | .notification-container.new:hover { 71 | background-color: #FFFFFF; 72 | } 73 | .notification { 74 | margin: 2px; 75 | padding: 7px; 76 | } 77 | .notification.new { 78 | background-color: #F0F0FF; 79 | } 80 | #notificationsBody a { 81 | color: #121212; 82 | text-decoration: none; 83 | display: block; 84 | margin-right: 0; 85 | border-bottom: 1px solid #dedede; 86 | font-weight: normal; 87 | white-space: normal; 88 | } 89 | .notification-date { 90 | color: #628DB6; 91 | font-style: italic; 92 | font-size: 10px; 93 | } 94 | .notification-title { 95 | font-weight: bold; 96 | color: #3E5B76; 97 | font-size: 12px; 98 | } 99 | .notification-content { 100 | color: #666666; 101 | display:block; 102 | word-wrap: break-word; 103 | white-space: normal; 104 | font-size: 11px; 105 | padding-left: 10px; 106 | } 107 | 108 | a.view-notification { 109 | float: right; 110 | } 111 | 112 | .push-notification { 113 | background-color: #333; 114 | position: fixed; 115 | right: 20px; 116 | bottom: 20px; 117 | color: #fff; 118 | padding: 15px 15px 15px 30px; 119 | -webkit-border-top-right-radius: 5px; 120 | -moz-border-radius: 5px; 121 | border-radius: 5px; 122 | background-repeat: no-repeat; 123 | background-position: 7px center; 124 | vertical-align: middle; 125 | box-shadow: 4px 4px 4px #000; 126 | -webkit-box-shadow: 4px 4px 4px #000; 127 | -moz-box-shadow: 4px 4px 4px #000; 128 | } 129 | -------------------------------------------------------------------------------- /config/locales/cs.yml: -------------------------------------------------------------------------------- 1 | cs: 2 | field_app_notification: "Notifikace v aplikaci" 3 | field_app_notification_desktop: "Použít desktop notifikace, jestliže to můj prohlížeč podporuje" 4 | notifications: "Notifikace" 5 | see_all: "Všechny" 6 | notifications_events_settings: "Nastavení udalostí notifikací" 7 | issue_added: "Přidání úkolu" 8 | issue_updated: "Změna úkolu" 9 | issue_note_added: "Přidána poznámka k úkolu" 10 | issue_status_updated: "Změna statusu" 11 | issue_assigned_to_updated: "Přiřazení úkolu" 12 | issue_priority_updated: "Změna priority" 13 | faye_server_adress: "Adresa faye serveru (nechte prázdné pro vypnuté)" 14 | mark_as_seen: "Označit jako zobrazené" 15 | mark_all_as_seen: "Označit vše jako zobrazené" 16 | viewed_notification: "zobrazené notifikace" 17 | new_notification: "nové notifikace" 18 | filter_button: "Filtrovat" 19 | view_app_notifications: "Zobrazit notifikace" 20 | unreachable_faye_server: "Váš Faye server je nedostupný" 21 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # English strings go here for Rails i18n 2 | en: 3 | field_app_notification: "In app notifications" 4 | field_app_notification_desktop: "Use desktop notifications if my browser supports it" 5 | notifications: "Notifications" 6 | see_all: "See All" 7 | notifications_events_settings: "In app notifications events settings" 8 | issue_added: "Issue added" 9 | issue_updated: "Issue updated" 10 | issue_note_added: "Issue note added" 11 | issue_status_updated: "Issue status updated" 12 | issue_assigned_to_updated: "Issue assigned" 13 | issue_priority_updated: "Issue priority updated" 14 | faye_server_adress: "Address of faye server (leave empty for turned off)" 15 | mark_as_seen: "Mark as seen" 16 | mark_all_as_seen: "Mark all as seen" 17 | viewed_notification: "viewed notifications" 18 | new_notification: "new notifications" 19 | filter_button: "Filter" 20 | view_app_notifications: "View notifications" 21 | unreachable_faye_server: "Your Faye server is unreachable" 22 | -------------------------------------------------------------------------------- /config/locales/es.yml: -------------------------------------------------------------------------------- 1 | # Spanish strings go here for Rails i18n 2 | es: 3 | field_app_notification: "Notificaciones dentro de la aplicación" 4 | field_app_notification_desktop: "Usar notificaciones de escritorio si mi navegador lo soporta" 5 | notifications: "Notificaciones" 6 | see_all: "Ver todas" 7 | notifications_events_settings: "Configuración de eventos de notificaciones dentro de la aplicación" 8 | issue_added: "Petición agregada" 9 | issue_updated: "Petición actualizada" 10 | issue_note_added: "Nota agregada" 11 | issue_status_updated: "Estado de petición actualizado" 12 | issue_assigned_to_updated: "Petición asignada" 13 | issue_priority_updated: "Prioridad de petición actualizada" 14 | faye_server_adress: "Dirección de servidor Faye (dejar en blanco para desactivar)" 15 | mark_as_seen: "Marcar como vista" 16 | mark_all_as_seen: "Marcar todas como vistas" 17 | viewed_notification: "Notificaciones vistas" 18 | new_notification: "Notificaciones nuevas" 19 | filter_button: "Filtrar" 20 | view_app_notifications: "Ver notificaciones" 21 | unreachable_faye_server: "Su servidor Faye es inaccesible" 22 | -------------------------------------------------------------------------------- /config/locales/fr.yml: -------------------------------------------------------------------------------- 1 | # French strings go here for Rails i18n 2 | fr: 3 | field_app_notification: "Notifications intégrées" 4 | field_app_notification_desktop: "Utiliser les notifications de bureau si mon navigateur les supporte" 5 | notifications: "Notifications" 6 | see_all: "Tout voir" 7 | notifications_events_settings: "Paramétrage des notifications intégrées" 8 | issue_added: "Demande ajoutée" 9 | issue_updated: "Demande mise à jour" 10 | issue_note_added: "Note de demande ajoutée" 11 | issue_status_updated: "Status de demande mis à jour" 12 | issue_assigned_to_updated: "Demande assignée" 13 | issue_priority_updated: "Priorité de demande mise à jour" 14 | faye_server_adress: "Adresse du serveur Faye (laisser vide pour désactiver)" 15 | mark_as_seen: "Marquer comme vu" 16 | mark_all_as_seen: "Tout marquer comme vu" 17 | viewed_notification: "Notifications vues" 18 | new_notification: "Notifications nouvelles" 19 | filter_button: "Filtrer" 20 | view_app_notifications: "Voir les notifications" 21 | unreachable_faye_server: "Votre server Faye est inaccessible" 22 | -------------------------------------------------------------------------------- /config/locales/ru.yml: -------------------------------------------------------------------------------- 1 | # Russian translation 2 | ru: 3 | field_app_notification: "Уведомления в приложении" 4 | field_app_notification_desktop: "Использовать уведомления рабочего стола при поддержке браузером" 5 | notifications: "Уведомления" 6 | see_all: "Просмотреть все" 7 | notifications_events_settings: "Настройки событий уведомлений в приложении" 8 | issue_added: "Задача добавлена" 9 | issue_updated: "Задача обновлена" 10 | issue_note_added: "Добавлено примечание к задаче" 11 | issue_status_updated: "Обновлен статус задачи" 12 | issue_assigned_to_updated: "Задача назначена" 13 | issue_priority_updated: "Обновлен приоритет задачи" 14 | faye_server_adress: "Адрес сервера faye (не заполнять, если не используемтся)" 15 | mark_as_seen: "Пометить как просмотренное" 16 | mark_all_as_seen: "Пометить все как просмотренное" 17 | viewed_notification: "просмотренные уведомления" 18 | new_notification: "новые уведомления" 19 | filter_button: "Фильтр" 20 | view_app_notifications: "Просмотр уведомлений" 21 | -------------------------------------------------------------------------------- /config/locales/tr.yml: -------------------------------------------------------------------------------- 1 | # Turkish strings go here for Rails i18n 2 | tr: 3 | field_app_notification: "Uygulama içi bildirimler" 4 | field_app_notification_desktop: "Tarayıcım destekliyorsa masaüstü bildirmini kullan" 5 | notifications: "Bildirimler" 6 | see_all: "Hepsini Gör" 7 | notifications_events_settings: "Uygulama içi bildirim hareketleri ayarları" 8 | issue_added: "İş eklendi" 9 | issue_updated: "İş güncellendi" 10 | issue_note_added: "İşe not eklendi" 11 | issue_status_updated: "İşin durumu güncellendi" 12 | issue_assigned_to_updated: "İş atandı" 13 | issue_priority_updated: "İş önceliği güncellendi" 14 | faye_server_adress: "faye sunucu adresi (kapalı için boş bırakın)" 15 | mark_as_seen: "Görüldü olarak işaretle" 16 | mark_all_as_seen: "Hepsini görüldü olarak işaretle" 17 | viewed_notification: "görülmüş bildirim" 18 | new_notification: "yeni bildirim" 19 | filter_button: "Filtre" 20 | view_app_notifications: "Bildirimleri Gör" 21 | unreachable_faye_server: "Faye sunucusuna erişilemiyor" 22 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | get 'app-notifications', :to => 'app_notifications#index' 2 | get 'app-notifications/:id', :to => 'app_notifications#view' 3 | post 'app-notifications/view-all', :to => 'app_notifications#view_all' 4 | -------------------------------------------------------------------------------- /db/migrate/001_create_app_notifications.rb: -------------------------------------------------------------------------------- 1 | class CreateAppNotifications < ActiveRecord::Migration 2 | def change 3 | create_table :app_notifications do |t| 4 | t.datetime :created_on 5 | t.boolean :viewed, default: false 6 | t.references :journal 7 | t.references :issue 8 | t.references :author 9 | t.references :recipient 10 | end 11 | add_index :app_notifications, :journal_id 12 | add_index :app_notifications, :issue_id 13 | add_index :app_notifications, :author_id 14 | add_index :app_notifications, :recipient_id 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/002_update_users.rb: -------------------------------------------------------------------------------- 1 | class UpdateUsers < ActiveRecord::Migration 2 | def self.up 3 | change_table :users do |t| 4 | t.column :app_notification, :boolean, :default => false 5 | end 6 | end 7 | 8 | def self.down 9 | change_table :users do |t| 10 | t.remove :app_notification 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/003_update_users2.rb: -------------------------------------------------------------------------------- 1 | class UpdateUsers2 < ActiveRecord::Migration 2 | def self.up 3 | change_table :users do |t| 4 | t.column :app_notification_desktop, :boolean, :default => false 5 | end 6 | end 7 | 8 | def self.down 9 | change_table :users do |t| 10 | t.remove :app_notification_desktop 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /faye.ru: -------------------------------------------------------------------------------- 1 | require 'faye' 2 | Faye::WebSocket.load_adapter('thin') 3 | 4 | bayeux = Faye::RackAdapter.new(:mount => '/faye', :timeout => 25) 5 | run bayeux -------------------------------------------------------------------------------- /faye_for_redmine: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: faye for Redmine 4 | # Required-Start: $local_fs $network 5 | # Required-Stop: $local_fs $network 6 | # Should-Start: 7 | # Default-Start: 2 3 4 5 8 | # Default-Stop: 0 1 6 9 | # Short-Description: Starts faye server for Redmine 10 | # Description: Faye is a push server which is used by the Redmine's plugin 11 | # in_app_notification 12 | ### END INIT INFO 13 | 14 | set -e 15 | 16 | ##### 17 | # Modify the variables to fit your needs 18 | ##### 19 | 20 | ### 21 | # Must modify 22 | ### 23 | IN_APP_NOTIFICATION_ROOT_PATH=/root/path/to/your/redmine_app_notifications 24 | ### 25 | 26 | ### 27 | # Probably need to modify 28 | ### 29 | SUDO_USER=www-data 30 | ### 31 | 32 | ### 33 | # Not necessary to modify 34 | ### 35 | FAYE_RU=faye.ru 36 | PID=thin.pid 37 | REDMINE_ENVIRONMENT=production 38 | PORT=9292 39 | ### 40 | 41 | CMD="cd $IN_APP_NOTIFICATION_ROOT_PATH && rackup $FAYE_RU -D -P $PID -E $REDMINE_ENVIRONMENT -s thin -p $PORT -o 0.0.0.0" 42 | 43 | set -u 44 | 45 | do_start () { 46 | export RAILS_ENV="$REDMINE_ENVIRONMENT" 47 | run "$CMD" 48 | } 49 | 50 | do_stop () { 51 | run "cd $IN_APP_NOTIFICATION_ROOT_PATH && echo '' >> $PID && pkill -9 -F $PID" 52 | } 53 | 54 | run () { 55 | if [ "$(id -un)" = "$SUDO_USER" ]; then 56 | eval "$1" 57 | else 58 | su -c "$1" - $SUDO_USER 59 | fi 60 | } 61 | 62 | case "$1" in 63 | start) 64 | do_start 65 | ;; 66 | stop) 67 | do_stop 68 | ;; 69 | restart) 70 | do_stop; do_start 71 | ;; 72 | *) 73 | echo "Usage: $0 [start|stop|restart]" >&2 74 | exit 1 75 | ;; 76 | esac 77 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | Redmine::Plugin.register :redmine_app_notifications do 2 | name 'Redmine App Notifications plugin' 3 | author 'Michal Vanzura' 4 | description 'App notifications plugin provides simple in application notifications. It can replace default e-mail notifications.' 5 | version '1.0' 6 | url 'https://github.com/MichalVanzura/redmine_app_notifications' 7 | author_url 'https://github.com/MichalVanzura/redmine_app_notifications' 8 | 9 | menu :top_menu, :app_notifications, { :controller => 'app_notifications', :action => 'index' }, { 10 | :caption => :notifications, 11 | :last => true, 12 | :if => Proc.new { User.current.app_notification }, 13 | :html => {:id => 'notificationsLink'} 14 | } 15 | 16 | menu :top_menu, :app_notifications_count, { :controller => 'app_notifications', :action => 'index' }, { 17 | :caption => Proc.new { AppNotification.where(recipient_id: User.current.id, viewed: false).count.to_s }, 18 | :last => true, 19 | :if => Proc.new { User.current.app_notification && AppNotification.where(recipient_id: User.current.id, viewed: false).count > 0 }, 20 | :html => {:id => 'notification_count'} 21 | } 22 | 23 | settings :default => { 24 | 'issue_added' => 'on', 25 | 'issue_updated' => 'on', 26 | 'issue_note_added' => 'on', 27 | 'issue_status_updated' => 'on', 28 | 'issue_assigned_to_updated' => 'on', 29 | 'issue_priority_updated' => 'on', 30 | 'faye_server_adress' => 'http://ip_address_or_name_of_your_server:9292/faye' 31 | }, :partial => 'settings/app_notifications_settings' 32 | end 33 | 34 | require_dependency 'app_notifications_hook_listener' 35 | require_dependency 'app_notifications_account_patch' 36 | require_dependency 'app_notifications_issues_patch' 37 | require_dependency 'app_notifications_journals_patch' 38 | -------------------------------------------------------------------------------- /lib/app_notifications_account_patch.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'my_controller' 2 | 3 | module AppNotificationsAccountPatch 4 | def self.included(base) # :nodoc: 5 | base.send(:include, InstanceMethods) 6 | 7 | base.class_eval do 8 | unloadable # Send unloadable so it will not be unloaded in development 9 | 10 | alias_method_chain :account, :in_app_option 11 | end 12 | end 13 | 14 | module InstanceMethods 15 | def account_with_in_app_option 16 | account = account_without_in_app_option 17 | User.safe_attributes 'app_notification', 'app_notification_desktop' 18 | return account 19 | end 20 | end 21 | end 22 | 23 | MyController.send(:include, AppNotificationsAccountPatch) 24 | -------------------------------------------------------------------------------- /lib/app_notifications_hook_listener.rb: -------------------------------------------------------------------------------- 1 | class AppNotificationsHookListener < Redmine::Hook::ViewListener 2 | render_on :view_my_account_preferences, :partial => "app_notifications/my_account_preferences" 3 | render_on :view_layouts_base_html_head, :partial => "app_notifications/layouts_base_html_head" 4 | end -------------------------------------------------------------------------------- /lib/app_notifications_issues_patch.rb: -------------------------------------------------------------------------------- 1 | module AppNotificationsIssuesPatch 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | after_create :create_app_notifications_after_create_issue 6 | end 7 | 8 | def create_app_notifications_after_create_issue 9 | if Setting.plugin_redmine_app_notifications.include?('issue_added') 10 | to_users = notified_users 11 | cc_users = notified_watchers - to_users 12 | @users = to_users + cc_users 13 | 14 | @users.each do |user| 15 | if user.app_notification && user.id != author.id 16 | notification = AppNotification.create({ 17 | :issue_id => id, 18 | :author_id => author.id, 19 | :recipient_id => user.id, 20 | }) 21 | notification.deliver 22 | end 23 | end 24 | end 25 | end 26 | end 27 | 28 | Issue.send(:include, AppNotificationsIssuesPatch) 29 | -------------------------------------------------------------------------------- /lib/app_notifications_journals_patch.rb: -------------------------------------------------------------------------------- 1 | module AppNotificationsJournalsPatch 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | after_create :create_app_notifications_after_create_journal 6 | end 7 | 8 | def create_app_notifications_after_create_journal 9 | 10 | if notify? && (Setting.plugin_redmine_app_notifications.include?('issue_updated') || 11 | (Setting.plugin_redmine_app_notifications.include?('issue_note_added') && journal.notes.present?) || 12 | (Setting.plugin_redmine_app_notifications.include?('issue_status_updated') && journal.new_status.present?) || 13 | (Setting.plugin_redmine_app_notifications.include?('issue_assigned_to_updated') && journal.detail_for_attribute('assigned_to_id').present?) || 14 | (Setting.plugin_redmine_app_notifications.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?) 15 | ) 16 | issue = journalized.reload 17 | to_users = notified_users 18 | cc_users = notified_watchers - to_users 19 | issue = journalized 20 | @author = user 21 | @issue = issue 22 | @users = to_users + cc_users 23 | 24 | @users.each do |user| 25 | if user.app_notification && user.id != @author.id 26 | notification = AppNotification.create({ 27 | :journal_id => id, 28 | :issue_id => @issue.id, 29 | :author_id => @author.id, 30 | :recipient_id => user.id, 31 | }) 32 | notification.deliver 33 | end 34 | end 35 | end 36 | end 37 | end 38 | 39 | Journal.send(:include, AppNotificationsJournalsPatch) 40 | -------------------------------------------------------------------------------- /test/fixtures/app_notifications.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichalVanzura/redmine_app_notifications/39f7e3c21360e91664ccf8eb8e6bd37094ab8596/test/fixtures/app_notifications.yml -------------------------------------------------------------------------------- /test/functional/app_notifications_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class AppNotificationsControllerTest < ActionController::TestCase 4 | 5 | include IssuesHelper 6 | 7 | FactoryGirl.define do 8 | factory :user_001, class: User do 9 | created_on "2006-07-19 19:12:21 +02:00" 10 | status 1 11 | last_login_on "2006-07-19 22:57:52 +02:00" 12 | language "en" 13 | # password = admin 14 | salt "82090c953c4a0000a7db253b0691a6b4" 15 | hashed_password "b5b6ff9543bf1387374cdfa27a54c96d236a7150" 16 | updated_on "2006-07-19 22:57:52 +02:00" 17 | admin true 18 | mail "jane@somenet.foo" 19 | lastname "Smith" 20 | firstname "Jane" 21 | mail_notification "all" 22 | login "jane" 23 | type "User" 24 | end 25 | 26 | factory :user_002, class: User do 27 | created_on "2006-07-19 19:12:21 +02:00" 28 | status 1 29 | last_login_on "2006-07-19 22:57:52 +02:00" 30 | language "en" 31 | # password = jsmith 32 | salt "67eb4732624d5a7753dcea7ce0bb7d7d" 33 | hashed_password "bfbe06043353a677d0215b26a5800d128d5413bc" 34 | updated_on "2006-07-19 22:42:15 +02:00" 35 | admin false 36 | mail "asmith@somenet.foo" 37 | lastname "Smith" 38 | firstname "Arthur" 39 | mail_notification "all" 40 | login "asmith" 41 | type "User" 42 | end 43 | 44 | factory :project do 45 | created_on "2006-07-19 19:13:59 +02:00" 46 | name "eCookbook" 47 | updated_on "2006-07-19 22:53:01 +02:00" 48 | description "Recipes management application" 49 | homepage "http://ecookbook.somenet.foo/" 50 | is_public true 51 | identifier "ecookbook" 52 | end 53 | 54 | factory :issue_category do 55 | name "Printing" 56 | end 57 | 58 | factory :tracker do 59 | name "tracker" 60 | end 61 | 62 | factory :project_with_tracker, :parent => :project do 63 | name "project" 64 | trackers {[FactoryGirl.create(:tracker)]} 65 | end 66 | 67 | factory :issue_priority do 68 | name "priority" 69 | end 70 | 71 | factory :issue_status do 72 | name "status" 73 | end 74 | 75 | factory :issue_001, class: Issue do 76 | created_on "2015-02-03 13:10:07" 77 | updated_on "2015-02-03 13:10:55" 78 | association :priority, factory: :issue_priority, strategy: :build 79 | subject "Can't print recipes" 80 | association :category, factory: :issue_category, strategy: :build 81 | description "Unable to print recipes" 82 | association :status, factory: :issue_status, strategy: :build 83 | start_date "2015-02-03" 84 | due_date "2015-03-03" 85 | lock_version 3 86 | end 87 | 88 | factory :journal do 89 | created_on "2015-02-03 13:10:07" 90 | notes "Journal notes" 91 | journalized_type "Issue" 92 | end 93 | 94 | factory :notification_001, class: AppNotification do 95 | id 1 96 | created_on "2015-01-24 16:41:41" 97 | viewed false 98 | end 99 | 100 | factory :notification_002, class: AppNotification do 101 | id 2 102 | created_on "2015-02-03 13:10:07" 103 | viewed true 104 | end 105 | 106 | factory :notification_003, class: AppNotification do 107 | id 3 108 | created_on "2014-06-005 08:10:38" 109 | viewed true 110 | end 111 | 112 | factory :notification_004, class: AppNotification do 113 | id 4 114 | created_on "2014-06-005 08:15:58" 115 | viewed false 116 | end 117 | end 118 | 119 | def setup 120 | @user_001 = create(:user_001) 121 | @user_002 = create(:user_002) 122 | @project = create(:project_with_tracker) 123 | @issue_001 = create(:issue_001, project: @project, tracker: @project.trackers.first, author: @user_001, assigned_to: @user_002) 124 | @journal = create(:journal, user: @user_001, journalized: @issue_001) 125 | @notification_001 = create(:notification_001, author: @user_001, recipient: @user_002, issue: @issue_001) 126 | @notification_002 = create(:notification_002, author: @user_001, recipient: @user_002, issue: @issue_001) 127 | @notification_003 = create(:notification_003, author: @user_001, recipient: @user_002, issue: @issue_001, journal: @journal) 128 | @notification_004 = create(:notification_004, author: @user_001, recipient: @user_002, issue: @issue_001, journal: @journal) 129 | end 130 | 131 | def test_index 132 | get :index 133 | 134 | assert_response :success 135 | assert_template 'index' 136 | end 137 | 138 | def test_index_with_user 139 | @request.session[:user_id] = @user_002.id 140 | with_current_user(@user_002) do 141 | get :index 142 | 143 | assert_template partial: '_issue_add', count: 2 144 | assert_template partial: '_issue_edit', count: 2 145 | assert_equal 4, assigns(:app_notifications).count 146 | end 147 | end 148 | 149 | def test_index_with_user_filter_new 150 | @request.session[:user_id] = @user_002.id 151 | with_current_user(@user_002) do 152 | get :index, :new => true, :commit => 'Filter' 153 | 154 | assert_template partial: '_issue_add', count: 1 155 | assert_template partial: '_issue_edit', count: 1 156 | assert_equal 2, assigns(:app_notifications).count 157 | end 158 | end 159 | 160 | def test_index_with_user_filter_viewed 161 | @request.session[:user_id] = @user_002.id 162 | with_current_user(@user_002) do 163 | get :index, :viewed => true, :commit => 'Filter' 164 | 165 | assert_template partial: '_issue_add', count: 1 166 | assert_template partial: '_issue_edit', count: 1 167 | assert_equal 2, assigns(:app_notifications).count 168 | end 169 | end 170 | 171 | def test_index_xhr 172 | xhr :get, :index 173 | 174 | assert_response :success 175 | assert_template 'ajax' 176 | end 177 | 178 | def test_index_xhr_with_user 179 | @request.session[:user_id] = @user_002.id 180 | with_current_user(@user_002) do 181 | xhr :get, :index 182 | 183 | assert_template partial: '_issue_ajax_add', count: 2 184 | assert_template partial: '_issue_ajax_edit', count: 2 185 | assert_equal 4, assigns(:app_notifications).count 186 | end 187 | end 188 | 189 | def test_view 190 | @request.session[:user_id] = @user_002.id 191 | with_current_user(@user_002) do 192 | assert !@notification_001.viewed 193 | get :view, :id => @notification_001.id, :issue_id => @issue_001.id 194 | 195 | assert AppNotification.find(@notification_001.id).viewed 196 | assert_response :redirect 197 | end 198 | end 199 | 200 | def test_view_xhr 201 | @request.session[:user_id] = @user_002.id 202 | with_current_user(@user_002) do 203 | xhr :get, :view, :id => @notification_001.id, :issue_id => @issue_001.id 204 | 205 | assert_template partial: '_issue_add', count: 1 206 | end 207 | end 208 | 209 | def test_view_all 210 | assert !@notification_001.viewed 211 | assert !@notification_004.viewed 212 | 213 | @request.session[:user_id] = @user_002.id 214 | with_current_user(@user_002) do 215 | post :view_all 216 | 217 | assert AppNotification.find(@notification_001.id).viewed 218 | assert AppNotification.find(@notification_004.id).viewed 219 | assert_response :redirect 220 | end 221 | end 222 | end 223 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Load the Redmine helper 2 | require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper') 3 | 4 | class Test::Unit::TestCase 5 | include FactoryGirl::Syntax::Methods 6 | end 7 | -------------------------------------------------------------------------------- /test/unit/app_notification_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class AppNotificationTest < ActiveSupport::TestCase 4 | 5 | FactoryGirl.define do 6 | factory :recipient, class: User do 7 | id 2 8 | end 9 | 10 | factory :issue, class: Issue do 11 | subject "Can't print recipes" 12 | description "Unable to print recipes" 13 | end 14 | 15 | factory :notification_001, class: AppNotification do 16 | id 1 17 | created_on "2015-01-24 16:41:41" 18 | viewed false 19 | association :recipient, strategy: :build 20 | association :issue, strategy: :build 21 | end 22 | end 23 | 24 | def setup 25 | @notification_001 = build_stubbed(:notification_001) 26 | end 27 | 28 | def test_deliver 29 | response = @notification_001.deliver("message") 30 | assert_equal response.code, "200" 31 | assert response.body.include? "notifications/private/2" 32 | end 33 | end 34 | --------------------------------------------------------------------------------