├── 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 |
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 |
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 |
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 |
13 |
14 | <% if journal.private_notes? %>
15 | (<%= l(:field_private_notes) %>)
16 | <% end %>
17 |
18 |
19 | <% details_to_strings(notification.journal.visible_details(notification.author)).each do |string| %>
20 | - <%= string %>
21 | <% end %>
22 |
23 |
24 | <%= textilizable(journal, :notes, :only_path => false) %>
25 | <% end %>
26 |
--------------------------------------------------------------------------------
/app/views/settings/_app_notifications_settings.html.erb:
--------------------------------------------------------------------------------
1 |
14 |
15 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------