Added "Watch" functionality on issues. It allows users to receive mail notifications about issue changes.

For now, it's only usefull for users who are not members of the project, since members receive notifications for each issue (this behaviour will change).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@453 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Jean-Philippe Lang 2007-04-21 12:08:31 +00:00
parent 907f906ec6
commit 2fb84af3e9
21 changed files with 245 additions and 11 deletions

View File

@ -0,0 +1,38 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class WatchersController < ApplicationController
layout 'base'
before_filter :require_login, :find_project, :check_project_privacy
def add
@issue.add_watcher(logged_in_user)
redirect_to :controller => 'issues', :action => 'show', :id => @issue
end
def remove
@issue.remove_watcher(logged_in_user)
redirect_to :controller => 'issues', :action => 'show', :id => @issue
end
private
def find_project
@issue = Issue.find(params[:issue_id])
@project = @issue.project
end
end

View File

@ -0,0 +1,19 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module WatchersHelper
end

View File

@ -32,6 +32,8 @@ class Issue < ActiveRecord::Base
has_many :custom_values, :dependent => :delete_all, :as => :customized
has_many :custom_fields, :through => :custom_values
acts_as_watchable
validates_presence_of :subject, :description, :priority, :tracker, :author, :status
validates_inclusion_of :done_ratio, :in => 0..100
validates_associated :custom_values, :on => :update

View File

@ -32,6 +32,8 @@ class Mailer < ActionMailer::Base
# Sends to all project members
issue = journal.journalized
@recipients = issue.project.members.collect { |m| m.user.mail if m.user.mail_notification }.compact
# Watchers in cc
@cc = issue.watcher_recipients - @recipients
@from = Setting.mail_from
@subject = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] #{issue.status.name} - #{issue.subject}"
@body['issue'] = issue

23
app/models/watcher.rb Normal file
View File

@ -0,0 +1,23 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Watcher < ActiveRecord::Base
belongs_to :watchable, :polymorphic => true
belongs_to :user
validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id]
end

View File

@ -53,6 +53,13 @@ end %>
<div class="contextual">
<%= link_to_if_authorized l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue}, :class => 'icon icon-edit' %>
<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %>
<% if @logged_in_user %>
<% if @issue.watched_by?(@logged_in_user) %>
<%= link_to l(:button_unwatch), {:controller => 'watchers', :action => 'remove', :issue_id => @issue}, :class => 'icon icon-fav' %>
<% else %>
<%= link_to l(:button_watch), {:controller => 'watchers', :action => 'add', :issue_id => @issue}, :class => 'icon icon-fav' %>
<% end %>
<% end %>
<%= link_to_if_authorized l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id }, :class => 'icon icon-move' %>
<%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
</div>

View File

@ -82,4 +82,5 @@ GLoc.set_kcode
GLoc.load_localized_strings
GLoc.set_config(:raise_string_not_found_errors => false)
require 'redmine'

View File

@ -383,6 +383,8 @@ button_activate: Aktivieren
button_sort: Sortieren
button_log_time: Log time
button_rollback: Rollback to this version
button_watch: Watch
button_unwatch: Unwatch
status_active: aktiv
status_registered: angemeldet

View File

@ -383,6 +383,8 @@ button_activate: Activate
button_sort: Sort
button_log_time: Log time
button_rollback: Rollback to this version
button_watch: Watch
button_unwatch: Unwatch
status_active: active
status_registered: registered

View File

@ -383,6 +383,8 @@ button_activate: Activar
button_sort: Clasificar
button_log_time: Log time
button_rollback: Rollback to this version
button_watch: Watch
button_unwatch: Unwatch
status_active: active
status_registered: registered

View File

@ -383,6 +383,8 @@ button_activate: Activer
button_sort: Trier
button_log_time: Saisir temps
button_rollback: Revenir à cette version
button_watch: Surveiller
button_unwatch: Ne plus surveiller
status_active: actif
status_registered: enregistré

View File

@ -383,6 +383,8 @@ button_activate: Attiva
button_sort: Ordina
button_log_time: Log time
button_rollback: Rollback to this version
button_watch: Watch
button_unwatch: Unwatch
status_active: active
status_registered: registered

View File

@ -384,6 +384,8 @@ button_activate: 有効にする
button_sort: ソート
button_log_time: 時間を記録
button_rollback: このバージョンにロールバック
button_watch: Watch
button_unwatch: Unwatch
status_active: 有効
status_registered: 登録

View File

@ -386,6 +386,8 @@ button_activate: 激活
button_sort: 排序
button_log_time: 登记工时
button_rollback: Rollback to this version
button_watch: Watch
button_unwatch: Unwatch
status_active: 激活
status_registered: 已注册

View File

@ -1,11 +1,2 @@
module Redmine
module VERSION #:nodoc:
MAJOR = 0
MINOR = 5
TINY = 0
STRING= [MAJOR, MINOR, TINY].join('.')
def self.to_s; STRING end
end
end
require 'redmine/version'
require 'redmine/acts_as_watchable/init'

View File

@ -0,0 +1,3 @@
# Include hook code here
require File.dirname(__FILE__) + '/lib/acts_as_watchable'
ActiveRecord::Base.send(:include, Redmine::Acts::Watchable)

View File

@ -0,0 +1,53 @@
# ActsAsWatchable
module Redmine
module Acts
module Watchable
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def acts_as_watchable(options = {})
return if self.included_modules.include?(Redmine::Acts::Watchable::InstanceMethods)
send :include, Redmine::Acts::Watchable::InstanceMethods
class_eval do
has_many :watchers, :as => :watchable, :dependent => :delete_all
end
end
end
module InstanceMethods
def self.included(base)
base.extend ClassMethods
end
def add_watcher(user)
self.watchers << Watcher.new(:user => user)
end
def remove_watcher(user)
return nil unless user && user.is_a?(User)
Watcher.delete_all "watchable_type = '#{self.class}' AND watchable_id = #{self.id} AND user_id = #{user.id}"
end
def watched_by?(user)
!self.watchers.find(:first,
:conditions => ["#{Watcher.table_name}.user_id = ?", user.id]).nil?
end
def watcher_recipients
self.watchers.collect { |w| w.user.mail if w.user.mail_notification }.compact
end
module ClassMethods
def watched_by(user)
find(:all,
:include => :watchers,
:conditions => ["#{Watcher.table_name}.user_id = ?", user.id])
end
end
end
end
end
end

11
lib/redmine/version.rb Normal file
View File

@ -0,0 +1,11 @@
module Redmine
module VERSION #:nodoc:
MAJOR = 0
MINOR = 5
TINY = 0
STRING= [MAJOR, MINOR, TINY].join('.')
def self.to_s; STRING end
end
end

BIN
public/images/fav.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

View File

@ -158,6 +158,7 @@ vertical-align: middle;
.icon-time { background-image: url(../images/time.png); }
.icon-stats { background-image: url(../images/stats.png); }
.icon-warning { background-image: url(../images/warning.png); }
.icon-fav { background-image: url(../images/fav.png); }
.icon22-projects { background-image: url(../images/22x22/projects.png); }
.icon22-users { background-image: url(../images/22x22/users.png); }

69
test/unit/watcher_test.rb Normal file
View File

@ -0,0 +1,69 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require File.dirname(__FILE__) + '/../test_helper'
class WatcherTest < Test::Unit::TestCase
fixtures :issues, :users
def setup
@user = User.find(1)
@issue = Issue.find(1)
end
def test_watch
assert @issue.add_watcher(@user)
@issue.reload
assert @issue.watchers.detect { |w| w.user == @user }
end
def test_cant_watch_twice
assert @issue.add_watcher(@user)
assert !@issue.add_watcher(@user)
end
def test_watched_by
assert @issue.add_watcher(@user)
@issue.reload
assert @issue.watched_by?(@user)
assert Issue.watched_by(@user).include?(@issue)
end
def test_recipients
@issue.watchers.delete_all
@issue.reload
assert @issue.watcher_recipients.empty?
assert @issue.add_watcher(@user)
@user.mail_notification = true
@user.save
@issue.reload
assert @issue.watcher_recipients.include?(@user.mail)
@user.mail_notification = false
@user.save
@issue.reload
assert !@issue.watcher_recipients.include?(@user.mail)
end
def test_unwatch
assert @issue.add_watcher(@user)
@issue.reload
assert_equal 1, @issue.remove_watcher(@user)
end
end