Revision 1297:0a574315af3e .svn/pristine/6c

View differences:

.svn/pristine/6c/6c63de300d0e80bc0b6e3de12c96ec81bd4c86b4.svn-base
1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
module Redmine
19
  module Ciphering
20
    def self.included(base)
21
      base.extend ClassMethods
22
    end
23

  
24
    class << self
25
      def encrypt_text(text)
26
        if cipher_key.blank? || text.blank?
27
          text
28
        else
29
          c = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
30
          iv = c.random_iv
31
          c.encrypt
32
          c.key = cipher_key
33
          c.iv = iv
34
          e = c.update(text.to_s)
35
          e << c.final
36
          "aes-256-cbc:" + [e, iv].map {|v| Base64.encode64(v).strip}.join('--')
37
        end
38
      end
39

  
40
      def decrypt_text(text)
41
        if text && match = text.match(/\Aaes-256-cbc:(.+)\Z/)
42
          if cipher_key.blank?
43
            logger.error "Attempt to decrypt a ciphered text with no cipher key configured in config/configuration.yml" if logger
44
            return text
45
          end
46
          text = match[1]
47
          c = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
48
          e, iv = text.split("--").map {|s| Base64.decode64(s)}
49
          c.decrypt
50
          c.key = cipher_key
51
          c.iv = iv
52
          d = c.update(e)
53
          d << c.final
54
        else
55
          text
56
        end
57
      end
58

  
59
      def cipher_key
60
        key = Redmine::Configuration['database_cipher_key'].to_s
61
        key.blank? ? nil : Digest::SHA256.hexdigest(key)
62
      end
63
      
64
      def logger
65
        Rails.logger
66
      end
67
    end
68

  
69
    module ClassMethods
70
      def encrypt_all(attribute)
71
        transaction do
72
          all.each do |object|
73
            clear = object.send(attribute)
74
            object.send "#{attribute}=", clear
75
            raise(ActiveRecord::Rollback) unless object.save(:validation => false)
76
          end
77
        end ? true : false
78
      end
79

  
80
      def decrypt_all(attribute)
81
        transaction do
82
          all.each do |object|
83
            clear = object.send(attribute)
84
            object.send :write_attribute, attribute, clear
85
            raise(ActiveRecord::Rollback) unless object.save(:validation => false)
86
          end
87
        end
88
      end ? true : false
89
    end
90

  
91
    private
92

  
93
    # Returns the value of the given ciphered attribute
94
    def read_ciphered_attribute(attribute)
95
      Redmine::Ciphering.decrypt_text(read_attribute(attribute))
96
    end
97

  
98
    # Sets the value of the given ciphered attribute
99
    def write_ciphered_attribute(attribute, value)
100
      write_attribute(attribute, Redmine::Ciphering.encrypt_text(value))
101
    end
102
  end
103
end
.svn/pristine/6c/6c693aad7bd181db73cd90834dd1050e5cf2a77a.svn-base
1
--- 
2
custom_fields_001: 
3
  name: Database
4
  min_length: 0
5
  regexp: ""
6
  is_for_all: true
7
  is_filter: true
8
  type: IssueCustomField
9
  max_length: 0
10
  possible_values: 
11
  - MySQL
12
  - PostgreSQL
13
  - Oracle
14
  id: 1
15
  is_required: false
16
  field_format: list
17
  default_value: ""
18
  editable: true
19
  position: 2
20
custom_fields_002: 
21
  name: Searchable field
22
  min_length: 1
23
  regexp: ""
24
  is_for_all: true
25
  is_filter: true
26
  type: IssueCustomField
27
  max_length: 100
28
  possible_values: ""
29
  id: 2
30
  is_required: false
31
  field_format: string
32
  searchable: true
33
  default_value: "Default string"
34
  editable: true
35
  position: 1
36
custom_fields_003: 
37
  name: Development status
38
  min_length: 0
39
  regexp: ""
40
  is_for_all: false
41
  is_filter: true
42
  type: ProjectCustomField
43
  max_length: 0
44
  possible_values: 
45
  - Stable
46
  - Beta
47
  - Alpha
48
  - Planning
49
  id: 3
50
  is_required: false
51
  field_format: list
52
  default_value: ""
53
  editable: true
54
  position: 1
55
custom_fields_004: 
56
  name: Phone number
57
  min_length: 0
58
  regexp: ""
59
  is_for_all: false
60
  type: UserCustomField
61
  max_length: 0
62
  possible_values: ""
63
  id: 4
64
  is_required: false
65
  field_format: string
66
  default_value: ""
67
  editable: true
68
  position: 1
69
custom_fields_005: 
70
  name: Money
71
  min_length: 0
72
  regexp: ""
73
  is_for_all: false
74
  type: UserCustomField
75
  max_length: 0
76
  possible_values: ""
77
  id: 5
78
  is_required: false
79
  field_format: float
80
  default_value: ""
81
  editable: true
82
  position: 2
83
custom_fields_006: 
84
  name: Float field
85
  min_length: 0
86
  regexp: ""
87
  is_for_all: true
88
  type: IssueCustomField
89
  max_length: 0
90
  possible_values: ""
91
  id: 6
92
  is_required: false
93
  field_format: float
94
  default_value: ""
95
  editable: true
96
  position: 3
97
custom_fields_007: 
98
  name: Billable
99
  min_length: 0
100
  regexp: ""
101
  is_for_all: false
102
  is_filter: true
103
  type: TimeEntryActivityCustomField
104
  max_length: 0
105
  possible_values: ""
106
  id: 7
107
  is_required: false
108
  field_format: bool
109
  default_value: ""
110
  editable: true
111
  position: 1
112
custom_fields_008: 
113
  name: Custom date
114
  min_length: 0
115
  regexp: ""
116
  is_for_all: true
117
  is_filter: false
118
  type: IssueCustomField
119
  max_length: 0
120
  possible_values: ""
121
  id: 8
122
  is_required: false
123
  field_format: date
124
  default_value: ""
125
  editable: true
126
  position: 4
127
custom_fields_009: 
128
  name: Project 1 cf
129
  min_length: 0
130
  regexp: ""
131
  is_for_all: false
132
  is_filter: true
133
  type: IssueCustomField
134
  max_length: 0
135
  possible_values: ""
136
  id: 9
137
  is_required: false
138
  field_format: date
139
  default_value: ""
140
  editable: true
141
  position: 5
142
custom_fields_010: 
143
  name: Overtime
144
  min_length: 0
145
  regexp: ""
146
  is_for_all: false
147
  is_filter: false
148
  type: TimeEntryCustomField
149
  max_length: 0
150
  possible_values: ""
151
  id: 10
152
  is_required: false
153
  field_format: bool
154
  default_value: 0
155
  editable: true
156
  position: 1
157
custom_fields_011:
158
  id: 11
159
  name: Binary
160
  type: CustomField
161
  possible_values: 
162
  - !binary |
163
    SGXDqWzDp2prc2Tigqw2NTTDuQ==
164
  - Other value
165
  field_format: list
.svn/pristine/6c/6c96a40bb02084f0ca89601fc6cb8ef86565a45a.svn-base
1
<p><%= l(:mail_body_register) %><br />
2
<%= link_to h(@url), @url %></p>
.svn/pristine/6c/6cc533b0e56df4187bfb76b52dd325e1a95364a0.svn-base
1
function jsToolBar(e){if(!document.createElement){return}if(!e){return}if(typeof document["selection"]=="undefined"&&typeof e["setSelectionRange"]=="undefined"){return}this.textarea=e;this.editor=document.createElement("div");this.editor.className="jstEditor";this.textarea.parentNode.insertBefore(this.editor,this.textarea);this.editor.appendChild(this.textarea);this.toolbar=document.createElement("div");this.toolbar.className="jstElements";this.editor.parentNode.insertBefore(this.toolbar,this.editor);if(this.editor.addEventListener&&navigator.appVersion.match(/\bMSIE\b/)){this.handle=document.createElement("div");this.handle.className="jstHandle";var t=this.resizeDragStart;var n=this;this.handle.addEventListener("mousedown",function(e){t.call(n,e)},false);window.addEventListener("unload",function(){var e=n.handle.parentNode.removeChild(n.handle);delete n.handle},false);this.editor.parentNode.insertBefore(this.handle,this.editor.nextSibling)}this.context=null;this.toolNodes={}}function jsButton(e,t,n,r){if(typeof jsToolBar.strings=="undefined"){this.title=e||null}else{this.title=jsToolBar.strings[e]||e||null}this.fn=t||function(){};this.scope=n||null;this.className=r||null}function jsSpace(e){this.id=e||null;this.width=null}function jsCombo(e,t,n,r,i){this.title=e||null;this.options=t||null;this.scope=n||null;this.fn=r||function(){};this.className=i||null}jsButton.prototype.draw=function(){if(!this.scope)return null;var e=document.createElement("button");e.setAttribute("type","button");e.tabIndex=200;if(this.className)e.className=this.className;e.title=this.title;var t=document.createElement("span");t.appendChild(document.createTextNode(this.title));e.appendChild(t);if(this.icon!=undefined){e.style.backgroundImage="url("+this.icon+")"}if(typeof this.fn=="function"){var n=this;e.onclick=function(){try{n.fn.apply(n.scope,arguments)}catch(e){}return false}}return e};jsSpace.prototype.draw=function(){var e=document.createElement("span");if(this.id)e.id=this.id;e.appendChild(document.createTextNode(String.fromCharCode(160)));e.className="jstSpacer";if(this.width)e.style.marginRight=this.width+"px";return e};jsCombo.prototype.draw=function(){if(!this.scope||!this.options)return null;var e=document.createElement("select");if(this.className)e.className=className;e.title=this.title;for(var t in this.options){var n=document.createElement("option");n.value=t;n.appendChild(document.createTextNode(this.options[t]));e.appendChild(n)}var r=this;e.onchange=function(){try{r.fn.call(r.scope,this.value)}catch(e){alert(e)}return false};return e};jsToolBar.prototype={base_url:"",mode:"wiki",elements:{},help_link:"",getMode:function(){return this.mode},setMode:function(e){this.mode=e||"wiki"},switchMode:function(e){e=e||"wiki";this.draw(e)},setHelpLink:function(e){this.help_link=e},button:function(e){var t=this.elements[e];if(typeof t.fn[this.mode]!="function")return null;var n=new jsButton(t.title,t.fn[this.mode],this,"jstb_"+e);if(t.icon!=undefined)n.icon=t.icon;return n},space:function(e){var t=new jsSpace(e);if(this.elements[e].width!==undefined)t.width=this.elements[e].width;return t},combo:function(e){var t=this.elements[e];var n=t[this.mode].list.length;if(typeof t[this.mode].fn!="function"||n==0){return null}else{var r={};for(var i=0;i<n;i++){var s=t[this.mode].list[i];r[s]=t.options[s]}return new jsCombo(t.title,r,this,t[this.mode].fn)}},draw:function(e){this.setMode(e);while(this.toolbar.hasChildNodes()){this.toolbar.removeChild(this.toolbar.firstChild)}this.toolNodes={};var t=document.createElement("div");t.className="help";t.innerHTML=this.help_link;"<a href=\"/help/wiki_syntax.html\" onclick=\"window.open('/help/wiki_syntax.html', '', 'resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes'); return false;\">Aide</a>";this.toolbar.appendChild(t);var n,r,i;for(var s in this.elements){n=this.elements[s];var o=n.type==undefined||n.type==""||n.disabled!=undefined&&n.disabled||n.context!=undefined&&n.context!=null&&n.context!=this.context;if(!o&&typeof this[n.type]=="function"){r=this[n.type](s);if(r)i=r.draw();if(i){this.toolNodes[s]=i;this.toolbar.appendChild(i)}}}},singleTag:function(e,t){e=e||null;t=t||e;if(!e||!t){return}this.encloseSelection(e,t)},encloseLineSelection:function(e,t,n){this.textarea.focus();e=e||"";t=t||"";var r,i,s,o,u,a;if(typeof document["selection"]!="undefined"){s=document.selection.createRange().text}else if(typeof this.textarea["setSelectionRange"]!="undefined"){r=this.textarea.selectionStart;i=this.textarea.selectionEnd;o=this.textarea.scrollTop;r=this.textarea.value.substring(0,r).replace(/[^\r\n]*$/g,"").length;i=this.textarea.value.length-this.textarea.value.substring(i,this.textarea.value.length).replace(/^[^\r\n]*/,"").length;s=this.textarea.value.substring(r,i)}if(s.match(/ $/)){s=s.substring(0,s.length-1);t=t+" "}if(typeof n=="function"){a=s?n.call(this,s):n("")}else{a=s?s:""}u=e+a+t;if(typeof document["selection"]!="undefined"){document.selection.createRange().text=u;var f=this.textarea.createTextRange();f.collapse(false);f.move("character",-t.length);f.select()}else if(typeof this.textarea["setSelectionRange"]!="undefined"){this.textarea.value=this.textarea.value.substring(0,r)+u+this.textarea.value.substring(i);if(s){this.textarea.setSelectionRange(r+u.length,r+u.length)}else{this.textarea.setSelectionRange(r+e.length,r+e.length)}this.textarea.scrollTop=o}},encloseSelection:function(e,t,n){this.textarea.focus();e=e||"";t=t||"";var r,i,s,o,u,a;if(typeof document["selection"]!="undefined"){s=document.selection.createRange().text}else if(typeof this.textarea["setSelectionRange"]!="undefined"){r=this.textarea.selectionStart;i=this.textarea.selectionEnd;o=this.textarea.scrollTop;s=this.textarea.value.substring(r,i)}if(s.match(/ $/)){s=s.substring(0,s.length-1);t=t+" "}if(typeof n=="function"){a=s?n.call(this,s):n("")}else{a=s?s:""}u=e+a+t;if(typeof document["selection"]!="undefined"){document.selection.createRange().text=u;var f=this.textarea.createTextRange();f.collapse(false);f.move("character",-t.length);f.select()}else if(typeof this.textarea["setSelectionRange"]!="undefined"){this.textarea.value=this.textarea.value.substring(0,r)+u+this.textarea.value.substring(i);if(s){this.textarea.setSelectionRange(r+u.length,r+u.length)}else{this.textarea.setSelectionRange(r+e.length,r+e.length)}this.textarea.scrollTop=o}},stripBaseURL:function(e){if(this.base_url!=""){var t=e.indexOf(this.base_url);if(t==0){e=e.substr(this.base_url.length)}}return e}};jsToolBar.prototype.resizeSetStartH=function(){this.dragStartH=this.textarea.offsetHeight+0};jsToolBar.prototype.resizeDragStart=function(e){var t=this;this.dragStartY=e.clientY;this.resizeSetStartH();document.addEventListener("mousemove",this.dragMoveHdlr=function(e){t.resizeDragMove(e)},false);document.addEventListener("mouseup",this.dragStopHdlr=function(e){t.resizeDragStop(e)},false)};jsToolBar.prototype.resizeDragMove=function(e){this.textarea.style.height=this.dragStartH+e.clientY-this.dragStartY+"px"};jsToolBar.prototype.resizeDragStop=function(e){document.removeEventListener("mousemove",this.dragMoveHdlr,false);document.removeEventListener("mouseup",this.dragStopHdlr,false)};jsToolBar.prototype.elements.strong={type:"button",title:"Strong",fn:{wiki:function(){this.singleTag("*")}}};jsToolBar.prototype.elements.em={type:"button",title:"Italic",fn:{wiki:function(){this.singleTag("_")}}};jsToolBar.prototype.elements.ins={type:"button",title:"Underline",fn:{wiki:function(){this.singleTag("+")}}};jsToolBar.prototype.elements.del={type:"button",title:"Deleted",fn:{wiki:function(){this.singleTag("-")}}};jsToolBar.prototype.elements.code={type:"button",title:"Code",fn:{wiki:function(){this.singleTag("@")}}};jsToolBar.prototype.elements.space1={type:"space"};jsToolBar.prototype.elements.h1={type:"button",title:"Heading 1",fn:{wiki:function(){this.encloseLineSelection("h1. ","",function(e){e=e.replace(/^h\d+\.\s+/,"");return e})}}};jsToolBar.prototype.elements.h2={type:"button",title:"Heading 2",fn:{wiki:function(){this.encloseLineSelection("h2. ","",function(e){e=e.replace(/^h\d+\.\s+/,"");return e})}}};jsToolBar.prototype.elements.h3={type:"button",title:"Heading 3",fn:{wiki:function(){this.encloseLineSelection("h3. ","",function(e){e=e.replace(/^h\d+\.\s+/,"");return e})}}};jsToolBar.prototype.elements.space2={type:"space"};jsToolBar.prototype.elements.ul={type:"button",title:"Unordered list",fn:{wiki:function(){this.encloseLineSelection("","",function(e){e=e.replace(/\r/g,"");return e.replace(/(\n|^)[#-]?\s*/g,"$1* ")})}}};jsToolBar.prototype.elements.ol={type:"button",title:"Ordered list",fn:{wiki:function(){this.encloseLineSelection("","",function(e){e=e.replace(/\r/g,"");return e.replace(/(\n|^)[*-]?\s*/g,"$1# ")})}}};jsToolBar.prototype.elements.space3={type:"space"};jsToolBar.prototype.elements.bq={type:"button",title:"Quote",fn:{wiki:function(){this.encloseLineSelection("","",function(e){e=e.replace(/\r/g,"");return e.replace(/(\n|^) *([^\n]*)/g,"$1> $2")})}}};jsToolBar.prototype.elements.unbq={type:"button",title:"Unquote",fn:{wiki:function(){this.encloseLineSelection("","",function(e){e=e.replace(/\r/g,"");return e.replace(/(\n|^) *[>]? *([^\n]*)/g,"$1$2")})}}};jsToolBar.prototype.elements.pre={type:"button",title:"Preformatted text",fn:{wiki:function(){this.encloseLineSelection("<pre>\n","\n</pre>")}}};jsToolBar.prototype.elements.space4={type:"space"};jsToolBar.prototype.elements.link={type:"button",title:"Wiki link",fn:{wiki:function(){this.encloseSelection("[[","]]")}}};jsToolBar.prototype.elements.img={type:"button",title:"Image",fn:{wiki:function(){this.encloseSelection("!","!")}}}
.svn/pristine/6c/6cd75d05f4deac0559250c87c2ffccdd03b546c2.svn-base
1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
require File.expand_path('../../test_helper', __FILE__)
19

  
20
class EnumerationsControllerTest < ActionController::TestCase
21
  fixtures :enumerations, :issues, :users
22

  
23
  def setup
24
    @request.session[:user_id] = 1 # admin
25
  end
26

  
27
  def test_index
28
    get :index
29
    assert_response :success
30
    assert_template 'index'
31
  end
32

  
33
  def test_index_should_require_admin
34
    @request.session[:user_id] = nil
35
    get :index
36
    assert_response 302
37
  end
38

  
39
  def test_new
40
    get :new, :type => 'IssuePriority'
41
    assert_response :success
42
    assert_template 'new'
43
    assert_kind_of IssuePriority, assigns(:enumeration)
44
    assert_tag 'input', :attributes => {:name => 'enumeration[type]', :value => 'IssuePriority'}
45
    assert_tag 'input', :attributes => {:name => 'enumeration[name]'}
46
  end
47

  
48
  def test_new_with_invalid_type_should_respond_with_404
49
    get :new, :type => 'UnknownType'
50
    assert_response 404
51
  end
52

  
53
  def test_create
54
    assert_difference 'IssuePriority.count' do
55
      post :create, :enumeration => {:type => 'IssuePriority', :name => 'Lowest'}
56
    end
57
    assert_redirected_to '/enumerations'
58
    e = IssuePriority.find_by_name('Lowest')
59
    assert_not_nil e
60
  end
61

  
62
  def test_create_with_failure
63
    assert_no_difference 'IssuePriority.count' do
64
      post :create, :enumeration => {:type => 'IssuePriority', :name => ''}
65
    end
66
    assert_response :success
67
    assert_template 'new'
68
  end
69

  
70
  def test_edit
71
    get :edit, :id => 6
72
    assert_response :success
73
    assert_template 'edit'
74
    assert_tag 'input', :attributes => {:name => 'enumeration[name]', :value => 'High'}
75
  end
76

  
77
  def test_edit_invalid_should_respond_with_404
78
    get :edit, :id => 999
79
    assert_response 404
80
  end
81

  
82
  def test_update
83
    assert_no_difference 'IssuePriority.count' do
84
      put :update, :id => 6, :enumeration => {:type => 'IssuePriority', :name => 'New name'}
85
    end
86
    assert_redirected_to '/enumerations'
87
    e = IssuePriority.find(6)
88
    assert_equal 'New name', e.name
89
  end
90

  
91
  def test_update_with_failure
92
    assert_no_difference 'IssuePriority.count' do
93
      put :update, :id => 6, :enumeration => {:type => 'IssuePriority', :name => ''}
94
    end
95
    assert_response :success
96
    assert_template 'edit'
97
  end
98

  
99
  def test_destroy_enumeration_not_in_use
100
    assert_difference 'IssuePriority.count', -1 do
101
      delete :destroy, :id => 7
102
    end
103
    assert_redirected_to :controller => 'enumerations', :action => 'index'
104
    assert_nil Enumeration.find_by_id(7)
105
  end
106

  
107
  def test_destroy_enumeration_in_use
108
    assert_no_difference 'IssuePriority.count' do
109
      delete :destroy, :id => 4
110
    end
111
    assert_response :success
112
    assert_template 'destroy'
113
    assert_not_nil Enumeration.find_by_id(4)
114
    assert_select 'select[name=reassign_to_id]' do
115
      assert_select 'option[value=6]', :text => 'High'
116
    end
117
  end
118

  
119
  def test_destroy_enumeration_in_use_with_reassignment
120
    issue = Issue.find(:first, :conditions => {:priority_id => 4})
121
    assert_difference 'IssuePriority.count', -1 do
122
      delete :destroy, :id => 4, :reassign_to_id => 6
123
    end
124
    assert_redirected_to :controller => 'enumerations', :action => 'index'
125
    assert_nil Enumeration.find_by_id(4)
126
    # check that the issue was reassign
127
    assert_equal 6, issue.reload.priority_id
128
  end
129
end
.svn/pristine/6c/6cea03c76318e826b03e4d334daa05b149e4780d.svn-base
1
# ActsAsWatchable
2
module Redmine
3
  module Acts
4
    module Watchable
5
      def self.included(base)
6
        base.extend ClassMethods
7
      end
8

  
9
      module ClassMethods
10
        def acts_as_watchable(options = {})
11
          return if self.included_modules.include?(Redmine::Acts::Watchable::InstanceMethods)
12
          class_eval do
13
            has_many :watchers, :as => :watchable, :dependent => :delete_all
14
            has_many :watcher_users, :through => :watchers, :source => :user, :validate => false
15

  
16
            scope :watched_by, lambda { |user_id|
17
              { :include => :watchers,
18
                :conditions => ["#{Watcher.table_name}.user_id = ?", user_id] }
19
            }
20
            attr_protected :watcher_ids, :watcher_user_ids
21
          end
22
          send :include, Redmine::Acts::Watchable::InstanceMethods
23
          alias_method_chain :watcher_user_ids=, :uniq_ids
24
        end
25
      end
26

  
27
      module InstanceMethods
28
        def self.included(base)
29
          base.extend ClassMethods
30
        end
31

  
32
        # Returns an array of users that are proposed as watchers
33
        def addable_watcher_users
34
          users = self.project.users.sort - self.watcher_users
35
          if respond_to?(:visible?)
36
            users.reject! {|user| !visible?(user)}
37
          end
38
          users
39
        end
40

  
41
        # Adds user as a watcher
42
        def add_watcher(user)
43
          self.watchers << Watcher.new(:user => user)
44
        end
45

  
46
        # Removes user from the watchers list
47
        def remove_watcher(user)
48
          return nil unless user && user.is_a?(User)
49
          Watcher.delete_all "watchable_type = '#{self.class}' AND watchable_id = #{self.id} AND user_id = #{user.id}"
50
        end
51

  
52
        # Adds/removes watcher
53
        def set_watcher(user, watching=true)
54
          watching ? add_watcher(user) : remove_watcher(user)
55
        end
56

  
57
        # Overrides watcher_user_ids= to make user_ids uniq
58
        def watcher_user_ids_with_uniq_ids=(user_ids)
59
          if user_ids.is_a?(Array)
60
            user_ids = user_ids.uniq
61
          end
62
          send :watcher_user_ids_without_uniq_ids=, user_ids
63
        end
64

  
65
        # Returns true if object is watched by +user+
66
        def watched_by?(user)
67
          !!(user && self.watcher_user_ids.detect {|uid| uid == user.id })
68
        end
69

  
70
        def notified_watchers
71
          notified = watcher_users.active
72
          notified.reject! {|user| user.mail.blank? || user.mail_notification == 'none'}
73
          if respond_to?(:visible?)
74
            notified.reject! {|user| !visible?(user)}
75
          end
76
          notified
77
        end
78

  
79
        # Returns an array of watchers' email addresses
80
        def watcher_recipients
81
          notified_watchers.collect(&:mail)
82
        end
83

  
84
        module ClassMethods; end
85
      end
86
    end
87
  end
88
end

Also available in: Unified diff