@@ -1,113 +1,111 | |||||
1 | # Redmine - project management software |
|
1 | # Redmine - project management software | |
2 | # Copyright (C) 2006-2016 Jean-Philippe Lang |
|
2 | # Copyright (C) 2006-2016 Jean-Philippe Lang | |
3 | # |
|
3 | # | |
4 | # This program is free software; you can redistribute it and/or |
|
4 | # This program is free software; you can redistribute it and/or | |
5 | # modify it under the terms of the GNU General Public License |
|
5 | # modify it under the terms of the GNU General Public License | |
6 | # as published by the Free Software Foundation; either version 2 |
|
6 | # as published by the Free Software Foundation; either version 2 | |
7 | # of the License, or (at your option) any later version. |
|
7 | # of the License, or (at your option) any later version. | |
8 | # |
|
8 | # | |
9 | # This program is distributed in the hope that it will be useful, |
|
9 | # This program is distributed in the hope that it will be useful, | |
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | # GNU General Public License for more details. |
|
12 | # GNU General Public License for more details. | |
13 | # |
|
13 | # | |
14 | # You should have received a copy of the GNU General Public License |
|
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 |
|
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. |
|
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
17 |
|
17 | |||
18 | class JournalsController < ApplicationController |
|
18 | class JournalsController < ApplicationController | |
19 | before_filter :find_journal, :only => [:edit, :update, :diff] |
|
19 | before_filter :find_journal, :only => [:edit, :update, :diff] | |
20 | before_filter :find_issue, :only => [:new] |
|
20 | before_filter :find_issue, :only => [:new] | |
21 | before_filter :find_optional_project, :only => [:index] |
|
21 | before_filter :find_optional_project, :only => [:index] | |
22 | before_filter :authorize, :only => [:new, :edit, :update, :diff] |
|
22 | before_filter :authorize, :only => [:new, :edit, :update, :diff] | |
23 | accept_rss_auth :index |
|
23 | accept_rss_auth :index | |
24 | menu_item :issues |
|
24 | menu_item :issues | |
25 |
|
25 | |||
26 | helper :issues |
|
26 | helper :issues | |
27 | helper :custom_fields |
|
27 | helper :custom_fields | |
28 | helper :queries |
|
28 | helper :queries | |
29 | include QueriesHelper |
|
29 | include QueriesHelper | |
30 | helper :sort |
|
30 | helper :sort | |
31 | include SortHelper |
|
31 | include SortHelper | |
32 |
|
32 | |||
33 | def index |
|
33 | def index | |
34 | retrieve_query |
|
34 | retrieve_query | |
35 | sort_init 'id', 'desc' |
|
35 | sort_init 'id', 'desc' | |
36 | sort_update(@query.sortable_columns) |
|
36 | sort_update(@query.sortable_columns) | |
37 | if @query.valid? |
|
37 | if @query.valid? | |
38 | @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC", |
|
38 | @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC", | |
39 | :limit => 25) |
|
39 | :limit => 25) | |
40 | end |
|
40 | end | |
41 | @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name) |
|
41 | @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name) | |
42 | render :layout => false, :content_type => 'application/atom+xml' |
|
42 | render :layout => false, :content_type => 'application/atom+xml' | |
43 | rescue ActiveRecord::RecordNotFound |
|
43 | rescue ActiveRecord::RecordNotFound | |
44 | render_404 |
|
44 | render_404 | |
45 | end |
|
45 | end | |
46 |
|
46 | |||
47 | def diff |
|
47 | def diff | |
48 | @issue = @journal.issue |
|
48 | @issue = @journal.issue | |
49 | if params[:detail_id].present? |
|
49 | if params[:detail_id].present? | |
50 | @detail = @journal.details.find_by_id(params[:detail_id]) |
|
50 | @detail = @journal.details.find_by_id(params[:detail_id]) | |
51 | else |
|
51 | else | |
52 | @detail = @journal.details.detect {|d| d.property == 'attr' && d.prop_key == 'description'} |
|
52 | @detail = @journal.details.detect {|d| d.property == 'attr' && d.prop_key == 'description'} | |
53 | end |
|
53 | end | |
54 | unless @issue && @detail |
|
54 | unless @issue && @detail | |
55 | render_404 |
|
55 | render_404 | |
56 | return false |
|
56 | return false | |
57 | end |
|
57 | end | |
58 | if @detail.property == 'cf' |
|
58 | if @detail.property == 'cf' | |
59 | unless @detail.custom_field && @detail.custom_field.visible_by?(@issue.project, User.current) |
|
59 | unless @detail.custom_field && @detail.custom_field.visible_by?(@issue.project, User.current) | |
60 | raise ::Unauthorized |
|
60 | raise ::Unauthorized | |
61 | end |
|
61 | end | |
62 | end |
|
62 | end | |
63 | @diff = Redmine::Helpers::Diff.new(@detail.value, @detail.old_value) |
|
63 | @diff = Redmine::Helpers::Diff.new(@detail.value, @detail.old_value) | |
64 | end |
|
64 | end | |
65 |
|
65 | |||
66 | def new |
|
66 | def new | |
67 | @journal = Journal.visible.find(params[:journal_id]) if params[:journal_id] |
|
67 | @journal = Journal.visible.find(params[:journal_id]) if params[:journal_id] | |
68 | if @journal |
|
68 | if @journal | |
69 | user = @journal.user |
|
69 | user = @journal.user | |
70 | text = @journal.notes |
|
70 | text = @journal.notes | |
71 | else |
|
71 | else | |
72 | user = @issue.author |
|
72 | user = @issue.author | |
73 | text = @issue.description |
|
73 | text = @issue.description | |
74 | end |
|
74 | end | |
75 | # Replaces pre blocks with [...] |
|
75 | # Replaces pre blocks with [...] | |
76 | text = text.to_s.strip.gsub(%r{<pre>(.*?)</pre>}m, '[...]') |
|
76 | text = text.to_s.strip.gsub(%r{<pre>(.*?)</pre>}m, '[...]') | |
77 | @content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> " |
|
77 | @content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> " | |
78 | @content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n" |
|
78 | @content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n" | |
79 | rescue ActiveRecord::RecordNotFound |
|
79 | rescue ActiveRecord::RecordNotFound | |
80 | render_404 |
|
80 | render_404 | |
81 | end |
|
81 | end | |
82 |
|
82 | |||
83 | def edit |
|
83 | def edit | |
84 | (render_403; return false) unless @journal.editable_by?(User.current) |
|
84 | (render_403; return false) unless @journal.editable_by?(User.current) | |
85 | respond_to do |format| |
|
85 | respond_to do |format| | |
86 | # TODO: implement non-JS journal update |
|
86 | # TODO: implement non-JS journal update | |
87 | format.js |
|
87 | format.js | |
88 | end |
|
88 | end | |
89 | end |
|
89 | end | |
90 |
|
90 | |||
91 | def update |
|
91 | def update | |
92 | (render_403; return false) unless @journal.editable_by?(User.current) |
|
92 | (render_403; return false) unless @journal.editable_by?(User.current) | |
93 | @journal.notes = params[:notes] if params[:notes] |
|
93 | @journal.safe_attributes = params[:journal] | |
94 | @journal.private_notes = params[:private_notes].present? |
|
94 | @journal.save | |
95 | (render_403; return false) if @journal.private_notes_changed? && User.current.allowed_to?(:set_notes_private, @journal.issue.project) == false |
|
|||
96 | @journal.save if @journal.changed? |
|
|||
97 | @journal.destroy if @journal.details.empty? && @journal.notes.blank? |
|
95 | @journal.destroy if @journal.details.empty? && @journal.notes.blank? | |
98 | call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params}) |
|
96 | call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params}) | |
99 | respond_to do |format| |
|
97 | respond_to do |format| | |
100 | format.html { redirect_to issue_path(@journal.journalized) } |
|
98 | format.html { redirect_to issue_path(@journal.journalized) } | |
101 | format.js |
|
99 | format.js | |
102 | end |
|
100 | end | |
103 | end |
|
101 | end | |
104 |
|
102 | |||
105 | private |
|
103 | private | |
106 |
|
104 | |||
107 | def find_journal |
|
105 | def find_journal | |
108 | @journal = Journal.visible.find(params[:id]) |
|
106 | @journal = Journal.visible.find(params[:id]) | |
109 | @project = @journal.journalized.project |
|
107 | @project = @journal.journalized.project | |
110 | rescue ActiveRecord::RecordNotFound |
|
108 | rescue ActiveRecord::RecordNotFound | |
111 | render_404 |
|
109 | render_404 | |
112 | end |
|
110 | end | |
113 | end |
|
111 | end |
@@ -1,67 +1,67 | |||||
1 | # encoding: utf-8 |
|
1 | # encoding: utf-8 | |
2 | # |
|
2 | # | |
3 | # Redmine - project management software |
|
3 | # Redmine - project management software | |
4 | # Copyright (C) 2006-2016 Jean-Philippe Lang |
|
4 | # Copyright (C) 2006-2016 Jean-Philippe Lang | |
5 | # |
|
5 | # | |
6 | # This program is free software; you can redistribute it and/or |
|
6 | # This program is free software; you can redistribute it and/or | |
7 | # modify it under the terms of the GNU General Public License |
|
7 | # modify it under the terms of the GNU General Public License | |
8 | # as published by the Free Software Foundation; either version 2 |
|
8 | # as published by the Free Software Foundation; either version 2 | |
9 | # of the License, or (at your option) any later version. |
|
9 | # of the License, or (at your option) any later version. | |
10 | # |
|
10 | # | |
11 | # This program is distributed in the hope that it will be useful, |
|
11 | # This program is distributed in the hope that it will be useful, | |
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 | # GNU General Public License for more details. |
|
14 | # GNU General Public License for more details. | |
15 | # |
|
15 | # | |
16 | # You should have received a copy of the GNU General Public License |
|
16 | # You should have received a copy of the GNU General Public License | |
17 | # along with this program; if not, write to the Free Software |
|
17 | # along with this program; if not, write to the Free Software | |
18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
19 |
|
19 | |||
20 | module JournalsHelper |
|
20 | module JournalsHelper | |
21 |
|
21 | |||
22 | # Returns the attachments of a journal that are displayed as thumbnails |
|
22 | # Returns the attachments of a journal that are displayed as thumbnails | |
23 | def journal_thumbnail_attachments(journal) |
|
23 | def journal_thumbnail_attachments(journal) | |
24 | ids = journal.details.select {|d| d.property == 'attachment' && d.value.present?}.map(&:prop_key) |
|
24 | ids = journal.details.select {|d| d.property == 'attachment' && d.value.present?}.map(&:prop_key) | |
25 | ids.any? ? Attachment.where(:id => ids).select(&:thumbnailable?) : [] |
|
25 | ids.any? ? Attachment.where(:id => ids).select(&:thumbnailable?) : [] | |
26 | end |
|
26 | end | |
27 |
|
27 | |||
28 | def render_notes(issue, journal, options={}) |
|
28 | def render_notes(issue, journal, options={}) | |
29 | content = '' |
|
29 | content = '' | |
30 | editable = User.current.logged? && (User.current.allowed_to?(:edit_issue_notes, issue.project) || (journal.user == User.current && User.current.allowed_to?(:edit_own_issue_notes, issue.project))) |
|
30 | editable = User.current.logged? && (User.current.allowed_to?(:edit_issue_notes, issue.project) || (journal.user == User.current && User.current.allowed_to?(:edit_own_issue_notes, issue.project))) | |
31 | links = [] |
|
31 | links = [] | |
32 | if !journal.notes.blank? |
|
32 | if !journal.notes.blank? | |
33 | links << link_to(l(:button_quote), |
|
33 | links << link_to(l(:button_quote), | |
34 | quoted_issue_path(issue, :journal_id => journal), |
|
34 | quoted_issue_path(issue, :journal_id => journal), | |
35 | :remote => true, |
|
35 | :remote => true, | |
36 | :method => 'post', |
|
36 | :method => 'post', | |
37 | :title => l(:button_quote), |
|
37 | :title => l(:button_quote), | |
38 | :class => 'icon-only icon-comment' |
|
38 | :class => 'icon-only icon-comment' | |
39 | ) if options[:reply_links] |
|
39 | ) if options[:reply_links] | |
40 | links << link_to(l(:button_edit), |
|
40 | links << link_to(l(:button_edit), | |
41 | edit_journal_path(journal), |
|
41 | edit_journal_path(journal), | |
42 | :remote => true, |
|
42 | :remote => true, | |
43 | :method => 'get', |
|
43 | :method => 'get', | |
44 | :title => l(:button_edit), |
|
44 | :title => l(:button_edit), | |
45 | :class => 'icon-only icon-edit' |
|
45 | :class => 'icon-only icon-edit' | |
46 | ) if editable |
|
46 | ) if editable | |
47 | links << link_to(l(:button_delete), |
|
47 | links << link_to(l(:button_delete), | |
48 | journal_path(journal, :notes => ""), |
|
48 | journal_path(journal, :journal => {:notes => ""}), | |
49 | :remote => true, |
|
49 | :remote => true, | |
50 | :method => 'put', :data => {:confirm => l(:text_are_you_sure)}, |
|
50 | :method => 'put', :data => {:confirm => l(:text_are_you_sure)}, | |
51 | :title => l(:button_delete), |
|
51 | :title => l(:button_delete), | |
52 | :class => 'icon-only icon-del' |
|
52 | :class => 'icon-only icon-del' | |
53 | ) if editable |
|
53 | ) if editable | |
54 | end |
|
54 | end | |
55 | content << content_tag('div', links.join(' ').html_safe, :class => 'contextual') unless links.empty? |
|
55 | content << content_tag('div', links.join(' ').html_safe, :class => 'contextual') unless links.empty? | |
56 | content << textilizable(journal, :notes) |
|
56 | content << textilizable(journal, :notes) | |
57 | css_classes = "wiki" |
|
57 | css_classes = "wiki" | |
58 | css_classes << " editable" if editable |
|
58 | css_classes << " editable" if editable | |
59 | content_tag('div', content.html_safe, :id => "journal-#{journal.id}-notes", :class => css_classes) |
|
59 | content_tag('div', content.html_safe, :id => "journal-#{journal.id}-notes", :class => css_classes) | |
60 | end |
|
60 | end | |
61 |
|
61 | |||
62 | def render_private_notes(journal) |
|
62 | def render_private_notes(journal) | |
63 | content = journal.private_notes? ? l(:field_is_private) : '' |
|
63 | content = journal.private_notes? ? l(:field_is_private) : '' | |
64 | css_classes = journal.private_notes? ? 'private' : '' |
|
64 | css_classes = journal.private_notes? ? 'private' : '' | |
65 | content_tag('span', content.html_safe, :id => "journal-#{journal.id}-private_notes", :class => css_classes) |
|
65 | content_tag('span', content.html_safe, :id => "journal-#{journal.id}-private_notes", :class => css_classes) | |
66 | end |
|
66 | end | |
67 | end |
|
67 | end |
@@ -1,305 +1,312 | |||||
1 | # Redmine - project management software |
|
1 | # Redmine - project management software | |
2 | # Copyright (C) 2006-2016 Jean-Philippe Lang |
|
2 | # Copyright (C) 2006-2016 Jean-Philippe Lang | |
3 | # |
|
3 | # | |
4 | # This program is free software; you can redistribute it and/or |
|
4 | # This program is free software; you can redistribute it and/or | |
5 | # modify it under the terms of the GNU General Public License |
|
5 | # modify it under the terms of the GNU General Public License | |
6 | # as published by the Free Software Foundation; either version 2 |
|
6 | # as published by the Free Software Foundation; either version 2 | |
7 | # of the License, or (at your option) any later version. |
|
7 | # of the License, or (at your option) any later version. | |
8 | # |
|
8 | # | |
9 | # This program is distributed in the hope that it will be useful, |
|
9 | # This program is distributed in the hope that it will be useful, | |
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | # GNU General Public License for more details. |
|
12 | # GNU General Public License for more details. | |
13 | # |
|
13 | # | |
14 | # You should have received a copy of the GNU General Public License |
|
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 |
|
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. |
|
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
17 |
|
17 | |||
18 | class Journal < ActiveRecord::Base |
|
18 | class Journal < ActiveRecord::Base | |
|
19 | include Redmine::SafeAttributes | |||
|
20 | ||||
19 | belongs_to :journalized, :polymorphic => true |
|
21 | belongs_to :journalized, :polymorphic => true | |
20 | # added as a quick fix to allow eager loading of the polymorphic association |
|
22 | # added as a quick fix to allow eager loading of the polymorphic association | |
21 | # since always associated to an issue, for now |
|
23 | # since always associated to an issue, for now | |
22 | belongs_to :issue, :foreign_key => :journalized_id |
|
24 | belongs_to :issue, :foreign_key => :journalized_id | |
23 |
|
25 | |||
24 | belongs_to :user |
|
26 | belongs_to :user | |
25 | has_many :details, :class_name => "JournalDetail", :dependent => :delete_all, :inverse_of => :journal |
|
27 | has_many :details, :class_name => "JournalDetail", :dependent => :delete_all, :inverse_of => :journal | |
26 | attr_accessor :indice |
|
28 | attr_accessor :indice | |
27 | attr_protected :id |
|
29 | attr_protected :id | |
28 |
|
30 | |||
29 | acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" }, |
|
31 | acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" }, | |
30 | :description => :notes, |
|
32 | :description => :notes, | |
31 | :author => :user, |
|
33 | :author => :user, | |
32 | :group => :issue, |
|
34 | :group => :issue, | |
33 | :type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' }, |
|
35 | :type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' }, | |
34 | :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}} |
|
36 | :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}} | |
35 |
|
37 | |||
36 | acts_as_activity_provider :type => 'issues', |
|
38 | acts_as_activity_provider :type => 'issues', | |
37 | :author_key => :user_id, |
|
39 | :author_key => :user_id, | |
38 | :scope => preload({:issue => :project}, :user). |
|
40 | :scope => preload({:issue => :project}, :user). | |
39 | joins("LEFT OUTER JOIN #{JournalDetail.table_name} ON #{JournalDetail.table_name}.journal_id = #{Journal.table_name}.id"). |
|
41 | joins("LEFT OUTER JOIN #{JournalDetail.table_name} ON #{JournalDetail.table_name}.journal_id = #{Journal.table_name}.id"). | |
40 | where("#{Journal.table_name}.journalized_type = 'Issue' AND" + |
|
42 | where("#{Journal.table_name}.journalized_type = 'Issue' AND" + | |
41 | " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')").uniq |
|
43 | " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')").uniq | |
42 |
|
44 | |||
43 | before_create :split_private_notes |
|
45 | before_create :split_private_notes | |
44 | after_create :send_notification |
|
46 | after_create :send_notification | |
45 |
|
47 | |||
46 | scope :visible, lambda {|*args| |
|
48 | scope :visible, lambda {|*args| | |
47 | user = args.shift || User.current |
|
49 | user = args.shift || User.current | |
48 | joins(:issue => :project). |
|
50 | joins(:issue => :project). | |
49 | where(Issue.visible_condition(user, *args)). |
|
51 | where(Issue.visible_condition(user, *args)). | |
50 | where("(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(user, :view_private_notes, *args)}))", false) |
|
52 | where("(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(user, :view_private_notes, *args)}))", false) | |
51 | } |
|
53 | } | |
52 |
|
54 | |||
|
55 | safe_attributes 'notes', | |||
|
56 | :if => lambda {|journal, user| journal.new_record? || journal.editable_by?(user)} | |||
|
57 | safe_attributes 'private_notes', | |||
|
58 | :if => lambda {|journal, user| user.allowed_to?(:set_notes_private, journal.project)} | |||
|
59 | ||||
53 | def initialize(*args) |
|
60 | def initialize(*args) | |
54 | super |
|
61 | super | |
55 | if journalized |
|
62 | if journalized | |
56 | if journalized.new_record? |
|
63 | if journalized.new_record? | |
57 | self.notify = false |
|
64 | self.notify = false | |
58 | else |
|
65 | else | |
59 | start |
|
66 | start | |
60 | end |
|
67 | end | |
61 | end |
|
68 | end | |
62 | end |
|
69 | end | |
63 |
|
70 | |||
64 | def save(*args) |
|
71 | def save(*args) | |
65 | journalize_changes |
|
72 | journalize_changes | |
66 | # Do not save an empty journal |
|
73 | # Do not save an empty journal | |
67 | (details.empty? && notes.blank?) ? false : super |
|
74 | (details.empty? && notes.blank?) ? false : super | |
68 | end |
|
75 | end | |
69 |
|
76 | |||
70 | # Returns journal details that are visible to user |
|
77 | # Returns journal details that are visible to user | |
71 | def visible_details(user=User.current) |
|
78 | def visible_details(user=User.current) | |
72 | details.select do |detail| |
|
79 | details.select do |detail| | |
73 | if detail.property == 'cf' |
|
80 | if detail.property == 'cf' | |
74 | detail.custom_field && detail.custom_field.visible_by?(project, user) |
|
81 | detail.custom_field && detail.custom_field.visible_by?(project, user) | |
75 | elsif detail.property == 'relation' |
|
82 | elsif detail.property == 'relation' | |
76 | Issue.find_by_id(detail.value || detail.old_value).try(:visible?, user) |
|
83 | Issue.find_by_id(detail.value || detail.old_value).try(:visible?, user) | |
77 | else |
|
84 | else | |
78 | true |
|
85 | true | |
79 | end |
|
86 | end | |
80 | end |
|
87 | end | |
81 | end |
|
88 | end | |
82 |
|
89 | |||
83 | def each_notification(users, &block) |
|
90 | def each_notification(users, &block) | |
84 | if users.any? |
|
91 | if users.any? | |
85 | users_by_details_visibility = users.group_by do |user| |
|
92 | users_by_details_visibility = users.group_by do |user| | |
86 | visible_details(user) |
|
93 | visible_details(user) | |
87 | end |
|
94 | end | |
88 | users_by_details_visibility.each do |visible_details, users| |
|
95 | users_by_details_visibility.each do |visible_details, users| | |
89 | if notes? || visible_details.any? |
|
96 | if notes? || visible_details.any? | |
90 | yield(users) |
|
97 | yield(users) | |
91 | end |
|
98 | end | |
92 | end |
|
99 | end | |
93 | end |
|
100 | end | |
94 | end |
|
101 | end | |
95 |
|
102 | |||
96 | # Returns the JournalDetail for the given attribute, or nil if the attribute |
|
103 | # Returns the JournalDetail for the given attribute, or nil if the attribute | |
97 | # was not updated |
|
104 | # was not updated | |
98 | def detail_for_attribute(attribute) |
|
105 | def detail_for_attribute(attribute) | |
99 | details.detect {|detail| detail.prop_key == attribute} |
|
106 | details.detect {|detail| detail.prop_key == attribute} | |
100 | end |
|
107 | end | |
101 |
|
108 | |||
102 | # Returns the new status if the journal contains a status change, otherwise nil |
|
109 | # Returns the new status if the journal contains a status change, otherwise nil | |
103 | def new_status |
|
110 | def new_status | |
104 | s = new_value_for('status_id') |
|
111 | s = new_value_for('status_id') | |
105 | s ? IssueStatus.find_by_id(s.to_i) : nil |
|
112 | s ? IssueStatus.find_by_id(s.to_i) : nil | |
106 | end |
|
113 | end | |
107 |
|
114 | |||
108 | def new_value_for(prop) |
|
115 | def new_value_for(prop) | |
109 | detail_for_attribute(prop).try(:value) |
|
116 | detail_for_attribute(prop).try(:value) | |
110 | end |
|
117 | end | |
111 |
|
118 | |||
112 | def editable_by?(usr) |
|
119 | def editable_by?(usr) | |
113 | usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project))) |
|
120 | usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project))) | |
114 | end |
|
121 | end | |
115 |
|
122 | |||
116 | def project |
|
123 | def project | |
117 | journalized.respond_to?(:project) ? journalized.project : nil |
|
124 | journalized.respond_to?(:project) ? journalized.project : nil | |
118 | end |
|
125 | end | |
119 |
|
126 | |||
120 | def attachments |
|
127 | def attachments | |
121 | journalized.respond_to?(:attachments) ? journalized.attachments : nil |
|
128 | journalized.respond_to?(:attachments) ? journalized.attachments : nil | |
122 | end |
|
129 | end | |
123 |
|
130 | |||
124 | # Returns a string of css classes |
|
131 | # Returns a string of css classes | |
125 | def css_classes |
|
132 | def css_classes | |
126 | s = 'journal' |
|
133 | s = 'journal' | |
127 | s << ' has-notes' unless notes.blank? |
|
134 | s << ' has-notes' unless notes.blank? | |
128 | s << ' has-details' unless details.blank? |
|
135 | s << ' has-details' unless details.blank? | |
129 | s << ' private-notes' if private_notes? |
|
136 | s << ' private-notes' if private_notes? | |
130 | s |
|
137 | s | |
131 | end |
|
138 | end | |
132 |
|
139 | |||
133 | def notify? |
|
140 | def notify? | |
134 | @notify != false |
|
141 | @notify != false | |
135 | end |
|
142 | end | |
136 |
|
143 | |||
137 | def notify=(arg) |
|
144 | def notify=(arg) | |
138 | @notify = arg |
|
145 | @notify = arg | |
139 | end |
|
146 | end | |
140 |
|
147 | |||
141 | def notified_users |
|
148 | def notified_users | |
142 | notified = journalized.notified_users |
|
149 | notified = journalized.notified_users | |
143 | if private_notes? |
|
150 | if private_notes? | |
144 | notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)} |
|
151 | notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)} | |
145 | end |
|
152 | end | |
146 | notified |
|
153 | notified | |
147 | end |
|
154 | end | |
148 |
|
155 | |||
149 | def recipients |
|
156 | def recipients | |
150 | notified_users.map(&:mail) |
|
157 | notified_users.map(&:mail) | |
151 | end |
|
158 | end | |
152 |
|
159 | |||
153 | def notified_watchers |
|
160 | def notified_watchers | |
154 | notified = journalized.notified_watchers |
|
161 | notified = journalized.notified_watchers | |
155 | if private_notes? |
|
162 | if private_notes? | |
156 | notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)} |
|
163 | notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)} | |
157 | end |
|
164 | end | |
158 | notified |
|
165 | notified | |
159 | end |
|
166 | end | |
160 |
|
167 | |||
161 | def watcher_recipients |
|
168 | def watcher_recipients | |
162 | notified_watchers.map(&:mail) |
|
169 | notified_watchers.map(&:mail) | |
163 | end |
|
170 | end | |
164 |
|
171 | |||
165 | # Sets @custom_field instance variable on journals details using a single query |
|
172 | # Sets @custom_field instance variable on journals details using a single query | |
166 | def self.preload_journals_details_custom_fields(journals) |
|
173 | def self.preload_journals_details_custom_fields(journals) | |
167 | field_ids = journals.map(&:details).flatten.select {|d| d.property == 'cf'}.map(&:prop_key).uniq |
|
174 | field_ids = journals.map(&:details).flatten.select {|d| d.property == 'cf'}.map(&:prop_key).uniq | |
168 | if field_ids.any? |
|
175 | if field_ids.any? | |
169 | fields_by_id = CustomField.where(:id => field_ids).inject({}) {|h, f| h[f.id] = f; h} |
|
176 | fields_by_id = CustomField.where(:id => field_ids).inject({}) {|h, f| h[f.id] = f; h} | |
170 | journals.each do |journal| |
|
177 | journals.each do |journal| | |
171 | journal.details.each do |detail| |
|
178 | journal.details.each do |detail| | |
172 | if detail.property == 'cf' |
|
179 | if detail.property == 'cf' | |
173 | detail.instance_variable_set "@custom_field", fields_by_id[detail.prop_key.to_i] |
|
180 | detail.instance_variable_set "@custom_field", fields_by_id[detail.prop_key.to_i] | |
174 | end |
|
181 | end | |
175 | end |
|
182 | end | |
176 | end |
|
183 | end | |
177 | end |
|
184 | end | |
178 | journals |
|
185 | journals | |
179 | end |
|
186 | end | |
180 |
|
187 | |||
181 | # Stores the values of the attributes and custom fields of the journalized object |
|
188 | # Stores the values of the attributes and custom fields of the journalized object | |
182 | def start |
|
189 | def start | |
183 | if journalized |
|
190 | if journalized | |
184 | @attributes_before_change = journalized.journalized_attribute_names.inject({}) do |h, attribute| |
|
191 | @attributes_before_change = journalized.journalized_attribute_names.inject({}) do |h, attribute| | |
185 | h[attribute] = journalized.send(attribute) |
|
192 | h[attribute] = journalized.send(attribute) | |
186 | h |
|
193 | h | |
187 | end |
|
194 | end | |
188 | @custom_values_before_change = journalized.custom_field_values.inject({}) do |h, c| |
|
195 | @custom_values_before_change = journalized.custom_field_values.inject({}) do |h, c| | |
189 | h[c.custom_field_id] = c.value |
|
196 | h[c.custom_field_id] = c.value | |
190 | h |
|
197 | h | |
191 | end |
|
198 | end | |
192 | end |
|
199 | end | |
193 | self |
|
200 | self | |
194 | end |
|
201 | end | |
195 |
|
202 | |||
196 | # Adds a journal detail for an attachment that was added or removed |
|
203 | # Adds a journal detail for an attachment that was added or removed | |
197 | def journalize_attachment(attachment, added_or_removed) |
|
204 | def journalize_attachment(attachment, added_or_removed) | |
198 | key = (added_or_removed == :removed ? :old_value : :value) |
|
205 | key = (added_or_removed == :removed ? :old_value : :value) | |
199 | details << JournalDetail.new( |
|
206 | details << JournalDetail.new( | |
200 | :property => 'attachment', |
|
207 | :property => 'attachment', | |
201 | :prop_key => attachment.id, |
|
208 | :prop_key => attachment.id, | |
202 | key => attachment.filename |
|
209 | key => attachment.filename | |
203 | ) |
|
210 | ) | |
204 | end |
|
211 | end | |
205 |
|
212 | |||
206 | # Adds a journal detail for an issue relation that was added or removed |
|
213 | # Adds a journal detail for an issue relation that was added or removed | |
207 | def journalize_relation(relation, added_or_removed) |
|
214 | def journalize_relation(relation, added_or_removed) | |
208 | key = (added_or_removed == :removed ? :old_value : :value) |
|
215 | key = (added_or_removed == :removed ? :old_value : :value) | |
209 | details << JournalDetail.new( |
|
216 | details << JournalDetail.new( | |
210 | :property => 'relation', |
|
217 | :property => 'relation', | |
211 | :prop_key => relation.relation_type_for(journalized), |
|
218 | :prop_key => relation.relation_type_for(journalized), | |
212 | key => relation.other_issue(journalized).try(:id) |
|
219 | key => relation.other_issue(journalized).try(:id) | |
213 | ) |
|
220 | ) | |
214 | end |
|
221 | end | |
215 |
|
222 | |||
216 | private |
|
223 | private | |
217 |
|
224 | |||
218 | # Generates journal details for attribute and custom field changes |
|
225 | # Generates journal details for attribute and custom field changes | |
219 | def journalize_changes |
|
226 | def journalize_changes | |
220 | # attributes changes |
|
227 | # attributes changes | |
221 | if @attributes_before_change |
|
228 | if @attributes_before_change | |
222 | journalized.journalized_attribute_names.each {|attribute| |
|
229 | journalized.journalized_attribute_names.each {|attribute| | |
223 | before = @attributes_before_change[attribute] |
|
230 | before = @attributes_before_change[attribute] | |
224 | after = journalized.send(attribute) |
|
231 | after = journalized.send(attribute) | |
225 | next if before == after || (before.blank? && after.blank?) |
|
232 | next if before == after || (before.blank? && after.blank?) | |
226 | add_attribute_detail(attribute, before, after) |
|
233 | add_attribute_detail(attribute, before, after) | |
227 | } |
|
234 | } | |
228 | end |
|
235 | end | |
229 | if @custom_values_before_change |
|
236 | if @custom_values_before_change | |
230 | # custom fields changes |
|
237 | # custom fields changes | |
231 | journalized.custom_field_values.each {|c| |
|
238 | journalized.custom_field_values.each {|c| | |
232 | before = @custom_values_before_change[c.custom_field_id] |
|
239 | before = @custom_values_before_change[c.custom_field_id] | |
233 | after = c.value |
|
240 | after = c.value | |
234 | next if before == after || (before.blank? && after.blank?) |
|
241 | next if before == after || (before.blank? && after.blank?) | |
235 |
|
242 | |||
236 | if before.is_a?(Array) || after.is_a?(Array) |
|
243 | if before.is_a?(Array) || after.is_a?(Array) | |
237 | before = [before] unless before.is_a?(Array) |
|
244 | before = [before] unless before.is_a?(Array) | |
238 | after = [after] unless after.is_a?(Array) |
|
245 | after = [after] unless after.is_a?(Array) | |
239 |
|
246 | |||
240 | # values removed |
|
247 | # values removed | |
241 | (before - after).reject(&:blank?).each do |value| |
|
248 | (before - after).reject(&:blank?).each do |value| | |
242 | add_custom_value_detail(c, value, nil) |
|
249 | add_custom_value_detail(c, value, nil) | |
243 | end |
|
250 | end | |
244 | # values added |
|
251 | # values added | |
245 | (after - before).reject(&:blank?).each do |value| |
|
252 | (after - before).reject(&:blank?).each do |value| | |
246 | add_custom_value_detail(c, nil, value) |
|
253 | add_custom_value_detail(c, nil, value) | |
247 | end |
|
254 | end | |
248 | else |
|
255 | else | |
249 | add_custom_value_detail(c, before, after) |
|
256 | add_custom_value_detail(c, before, after) | |
250 | end |
|
257 | end | |
251 | } |
|
258 | } | |
252 | end |
|
259 | end | |
253 | start |
|
260 | start | |
254 | end |
|
261 | end | |
255 |
|
262 | |||
256 | # Adds a journal detail for an attribute change |
|
263 | # Adds a journal detail for an attribute change | |
257 | def add_attribute_detail(attribute, old_value, value) |
|
264 | def add_attribute_detail(attribute, old_value, value) | |
258 | add_detail('attr', attribute, old_value, value) |
|
265 | add_detail('attr', attribute, old_value, value) | |
259 | end |
|
266 | end | |
260 |
|
267 | |||
261 | # Adds a journal detail for a custom field value change |
|
268 | # Adds a journal detail for a custom field value change | |
262 | def add_custom_value_detail(custom_value, old_value, value) |
|
269 | def add_custom_value_detail(custom_value, old_value, value) | |
263 | add_detail('cf', custom_value.custom_field_id, old_value, value) |
|
270 | add_detail('cf', custom_value.custom_field_id, old_value, value) | |
264 | end |
|
271 | end | |
265 |
|
272 | |||
266 | # Adds a journal detail |
|
273 | # Adds a journal detail | |
267 | def add_detail(property, prop_key, old_value, value) |
|
274 | def add_detail(property, prop_key, old_value, value) | |
268 | details << JournalDetail.new( |
|
275 | details << JournalDetail.new( | |
269 | :property => property, |
|
276 | :property => property, | |
270 | :prop_key => prop_key, |
|
277 | :prop_key => prop_key, | |
271 | :old_value => old_value, |
|
278 | :old_value => old_value, | |
272 | :value => value |
|
279 | :value => value | |
273 | ) |
|
280 | ) | |
274 | end |
|
281 | end | |
275 |
|
282 | |||
276 | def split_private_notes |
|
283 | def split_private_notes | |
277 | if private_notes? |
|
284 | if private_notes? | |
278 | if notes.present? |
|
285 | if notes.present? | |
279 | if details.any? |
|
286 | if details.any? | |
280 | # Split the journal (notes/changes) so we don't have half-private journals |
|
287 | # Split the journal (notes/changes) so we don't have half-private journals | |
281 | journal = Journal.new(:journalized => journalized, :user => user, :notes => nil, :private_notes => false) |
|
288 | journal = Journal.new(:journalized => journalized, :user => user, :notes => nil, :private_notes => false) | |
282 | journal.details = details |
|
289 | journal.details = details | |
283 | journal.save |
|
290 | journal.save | |
284 | self.details = [] |
|
291 | self.details = [] | |
285 | self.created_on = journal.created_on |
|
292 | self.created_on = journal.created_on | |
286 | end |
|
293 | end | |
287 | else |
|
294 | else | |
288 | # Blank notes should not be private |
|
295 | # Blank notes should not be private | |
289 | self.private_notes = false |
|
296 | self.private_notes = false | |
290 | end |
|
297 | end | |
291 | end |
|
298 | end | |
292 | true |
|
299 | true | |
293 | end |
|
300 | end | |
294 |
|
301 | |||
295 | def send_notification |
|
302 | def send_notification | |
296 | if notify? && (Setting.notified_events.include?('issue_updated') || |
|
303 | if notify? && (Setting.notified_events.include?('issue_updated') || | |
297 | (Setting.notified_events.include?('issue_note_added') && notes.present?) || |
|
304 | (Setting.notified_events.include?('issue_note_added') && notes.present?) || | |
298 | (Setting.notified_events.include?('issue_status_updated') && new_status.present?) || |
|
305 | (Setting.notified_events.include?('issue_status_updated') && new_status.present?) || | |
299 | (Setting.notified_events.include?('issue_assigned_to_updated') && detail_for_attribute('assigned_to_id').present?) || |
|
306 | (Setting.notified_events.include?('issue_assigned_to_updated') && detail_for_attribute('assigned_to_id').present?) || | |
300 | (Setting.notified_events.include?('issue_priority_updated') && new_value_for('priority_id').present?) |
|
307 | (Setting.notified_events.include?('issue_priority_updated') && new_value_for('priority_id').present?) | |
301 | ) |
|
308 | ) | |
302 | Mailer.deliver_issue_edit(self) |
|
309 | Mailer.deliver_issue_edit(self) | |
303 | end |
|
310 | end | |
304 | end |
|
311 | end | |
305 | end |
|
312 | end |
@@ -1,22 +1,24 | |||||
1 | <%= form_tag(journal_path(@journal), |
|
1 | <%= form_tag(journal_path(@journal), | |
2 | :remote => true, |
|
2 | :remote => true, | |
3 | :method => 'put', |
|
3 | :method => 'put', | |
4 | :id => "journal-#{@journal.id}-form") do %> |
|
4 | :id => "journal-#{@journal.id}-form") do %> | |
5 | <%= label_tag "notes", l(:description_notes), :class => "hidden-for-sighted" %> |
|
5 | <%= label_tag "notes", l(:description_notes), :class => "hidden-for-sighted" %> | |
6 |
<%= text_area_tag |
|
6 | <%= text_area_tag 'journal[notes]', @journal.notes, | |
7 | :id => "journal_#{@journal.id}_notes", |
|
7 | :id => "journal_#{@journal.id}_notes", | |
8 | :class => 'wiki-edit', |
|
8 | :class => 'wiki-edit', | |
9 | :rows => (@journal.notes.blank? ? 10 : [[10, @journal.notes.length / 50].max, 100].min) %> |
|
9 | :rows => (@journal.notes.blank? ? 10 : [[10, @journal.notes.length / 50].max, 100].min) %> | |
10 |
<% if @journal. |
|
10 | <% if @journal.safe_attribute? 'private_notes' %> | |
11 | <%= check_box_tag 'private_notes', '1', @journal.private_notes, :id => "journal_#{@journal.id}_private_notes" %> <label for="journal_<%= @journal.id %>_private_notes"><%= l(:field_private_notes) %></label> |
|
11 | <%= hidden_field_tag 'journal[private_notes]', '0' %> | |
|
12 | <%= check_box_tag 'journal[private_notes]', '1', @journal.private_notes, :id => "journal_#{@journal.id}_private_notes" %> | |||
|
13 | <label for="journal_<%= @journal.id %>_private_notes"><%= l(:field_private_notes) %></label> | |||
12 | <% end %> |
|
14 | <% end %> | |
13 | <%= call_hook(:view_journals_notes_form_after_notes, { :journal => @journal}) %> |
|
15 | <%= call_hook(:view_journals_notes_form_after_notes, { :journal => @journal}) %> | |
14 | <p><%= submit_tag l(:button_save) %> |
|
16 | <p><%= submit_tag l(:button_save) %> | |
15 | <%= preview_link preview_edit_issue_path(:project_id => @project, :id => @journal.issue), |
|
17 | <%= preview_link preview_edit_issue_path(:project_id => @project, :id => @journal.issue), | |
16 | "journal-#{@journal.id}-form", |
|
18 | "journal-#{@journal.id}-form", | |
17 | "journal_#{@journal.id}_preview" %> | |
|
19 | "journal_#{@journal.id}_preview" %> | | |
18 | <%= link_to l(:button_cancel), '#', :onclick => "$('#journal-#{@journal.id}-form').remove(); $('#journal-#{@journal.id}-notes').show(); return false;" %></p> |
|
20 | <%= link_to l(:button_cancel), '#', :onclick => "$('#journal-#{@journal.id}-form').remove(); $('#journal-#{@journal.id}-notes').show(); return false;" %></p> | |
19 |
|
21 | |||
20 | <div id="journal_<%= @journal.id %>_preview" class="wiki"></div> |
|
22 | <div id="journal_<%= @journal.id %>_preview" class="wiki"></div> | |
21 | <% end %> |
|
23 | <% end %> | |
22 | <%= wikitoolbar_for "journal_#{@journal.id}_notes" %> |
|
24 | <%= wikitoolbar_for "journal_#{@journal.id}_notes" %> |
@@ -1,254 +1,255 | |||||
1 | # Redmine - project management software |
|
1 | # Redmine - project management software | |
2 | # Copyright (C) 2006-2016 Jean-Philippe Lang |
|
2 | # Copyright (C) 2006-2016 Jean-Philippe Lang | |
3 | # |
|
3 | # | |
4 | # This program is free software; you can redistribute it and/or |
|
4 | # This program is free software; you can redistribute it and/or | |
5 | # modify it under the terms of the GNU General Public License |
|
5 | # modify it under the terms of the GNU General Public License | |
6 | # as published by the Free Software Foundation; either version 2 |
|
6 | # as published by the Free Software Foundation; either version 2 | |
7 | # of the License, or (at your option) any later version. |
|
7 | # of the License, or (at your option) any later version. | |
8 | # |
|
8 | # | |
9 | # This program is distributed in the hope that it will be useful, |
|
9 | # This program is distributed in the hope that it will be useful, | |
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | # GNU General Public License for more details. |
|
12 | # GNU General Public License for more details. | |
13 | # |
|
13 | # | |
14 | # You should have received a copy of the GNU General Public License |
|
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 |
|
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. |
|
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
17 |
|
17 | |||
18 | require File.expand_path('../../test_helper', __FILE__) |
|
18 | require File.expand_path('../../test_helper', __FILE__) | |
19 |
|
19 | |||
20 | class JournalsControllerTest < ActionController::TestCase |
|
20 | class JournalsControllerTest < ActionController::TestCase | |
21 | fixtures :projects, :users, :members, :member_roles, :roles, :issues, :journals, :journal_details, :enabled_modules, |
|
21 | fixtures :projects, :users, :members, :member_roles, :roles, :issues, :journals, :journal_details, :enabled_modules, | |
22 | :trackers, :issue_statuses, :enumerations, :custom_fields, :custom_values, :custom_fields_projects, :projects_trackers |
|
22 | :trackers, :issue_statuses, :enumerations, :custom_fields, :custom_values, :custom_fields_projects, :projects_trackers | |
23 |
|
23 | |||
24 | def setup |
|
24 | def setup | |
25 | User.current = nil |
|
25 | User.current = nil | |
26 | end |
|
26 | end | |
27 |
|
27 | |||
28 | def test_index |
|
28 | def test_index | |
29 | get :index, :project_id => 1 |
|
29 | get :index, :project_id => 1 | |
30 | assert_response :success |
|
30 | assert_response :success | |
31 | assert_not_nil assigns(:journals) |
|
31 | assert_not_nil assigns(:journals) | |
32 | assert_equal 'application/atom+xml', @response.content_type |
|
32 | assert_equal 'application/atom+xml', @response.content_type | |
33 | end |
|
33 | end | |
34 |
|
34 | |||
35 | def test_index_with_invalid_query_id |
|
35 | def test_index_with_invalid_query_id | |
36 | get :index, :project_id => 1, :query_id => 999 |
|
36 | get :index, :project_id => 1, :query_id => 999 | |
37 | assert_response 404 |
|
37 | assert_response 404 | |
38 | end |
|
38 | end | |
39 |
|
39 | |||
40 | def test_index_should_return_privates_notes_with_permission_only |
|
40 | def test_index_should_return_privates_notes_with_permission_only | |
41 | journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true, :user_id => 1) |
|
41 | journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true, :user_id => 1) | |
42 | @request.session[:user_id] = 2 |
|
42 | @request.session[:user_id] = 2 | |
43 |
|
43 | |||
44 | get :index, :project_id => 1 |
|
44 | get :index, :project_id => 1 | |
45 | assert_response :success |
|
45 | assert_response :success | |
46 | assert_include journal, assigns(:journals) |
|
46 | assert_include journal, assigns(:journals) | |
47 |
|
47 | |||
48 | Role.find(1).remove_permission! :view_private_notes |
|
48 | Role.find(1).remove_permission! :view_private_notes | |
49 | get :index, :project_id => 1 |
|
49 | get :index, :project_id => 1 | |
50 | assert_response :success |
|
50 | assert_response :success | |
51 | assert_not_include journal, assigns(:journals) |
|
51 | assert_not_include journal, assigns(:journals) | |
52 | end |
|
52 | end | |
53 |
|
53 | |||
54 | def test_index_should_show_visible_custom_fields_only |
|
54 | def test_index_should_show_visible_custom_fields_only | |
55 | Issue.destroy_all |
|
55 | Issue.destroy_all | |
56 | field_attributes = {:field_format => 'string', :is_for_all => true, :is_filter => true, :trackers => Tracker.all} |
|
56 | field_attributes = {:field_format => 'string', :is_for_all => true, :is_filter => true, :trackers => Tracker.all} | |
57 | @fields = [] |
|
57 | @fields = [] | |
58 | @fields << (@field1 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 1', :visible => true))) |
|
58 | @fields << (@field1 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 1', :visible => true))) | |
59 | @fields << (@field2 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 2', :visible => false, :role_ids => [1, 2]))) |
|
59 | @fields << (@field2 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 2', :visible => false, :role_ids => [1, 2]))) | |
60 | @fields << (@field3 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 3', :visible => false, :role_ids => [1, 3]))) |
|
60 | @fields << (@field3 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 3', :visible => false, :role_ids => [1, 3]))) | |
61 | @issue = Issue.generate!( |
|
61 | @issue = Issue.generate!( | |
62 | :author_id => 1, |
|
62 | :author_id => 1, | |
63 | :project_id => 1, |
|
63 | :project_id => 1, | |
64 | :tracker_id => 1, |
|
64 | :tracker_id => 1, | |
65 | :custom_field_values => {@field1.id => 'Value0', @field2.id => 'Value1', @field3.id => 'Value2'} |
|
65 | :custom_field_values => {@field1.id => 'Value0', @field2.id => 'Value1', @field3.id => 'Value2'} | |
66 | ) |
|
66 | ) | |
67 | @issue.init_journal(User.find(1)) |
|
67 | @issue.init_journal(User.find(1)) | |
68 | @issue.update_attribute :custom_field_values, {@field1.id => 'NewValue0', @field2.id => 'NewValue1', @field3.id => 'NewValue2'} |
|
68 | @issue.update_attribute :custom_field_values, {@field1.id => 'NewValue0', @field2.id => 'NewValue1', @field3.id => 'NewValue2'} | |
69 |
|
69 | |||
70 |
|
70 | |||
71 | user_with_role_on_other_project = User.generate! |
|
71 | user_with_role_on_other_project = User.generate! | |
72 | User.add_to_project(user_with_role_on_other_project, Project.find(2), Role.find(3)) |
|
72 | User.add_to_project(user_with_role_on_other_project, Project.find(2), Role.find(3)) | |
73 | users_to_test = { |
|
73 | users_to_test = { | |
74 | User.find(1) => [@field1, @field2, @field3], |
|
74 | User.find(1) => [@field1, @field2, @field3], | |
75 | User.find(3) => [@field1, @field2], |
|
75 | User.find(3) => [@field1, @field2], | |
76 | user_with_role_on_other_project => [@field1], # should see field1 only on Project 1 |
|
76 | user_with_role_on_other_project => [@field1], # should see field1 only on Project 1 | |
77 | User.generate! => [@field1], |
|
77 | User.generate! => [@field1], | |
78 | User.anonymous => [@field1] |
|
78 | User.anonymous => [@field1] | |
79 | } |
|
79 | } | |
80 |
|
80 | |||
81 | users_to_test.each do |user, visible_fields| |
|
81 | users_to_test.each do |user, visible_fields| | |
82 | get :index, :format => 'atom', :key => user.rss_key |
|
82 | get :index, :format => 'atom', :key => user.rss_key | |
83 | @fields.each_with_index do |field, i| |
|
83 | @fields.each_with_index do |field, i| | |
84 | if visible_fields.include?(field) |
|
84 | if visible_fields.include?(field) | |
85 | assert_select "content[type=html]", { :text => /NewValue#{i}/, :count => 1 }, "User #{user.id} was not able to view #{field.name} in API" |
|
85 | assert_select "content[type=html]", { :text => /NewValue#{i}/, :count => 1 }, "User #{user.id} was not able to view #{field.name} in API" | |
86 | else |
|
86 | else | |
87 | assert_select "content[type=html]", { :text => /NewValue#{i}/, :count => 0 }, "User #{user.id} was able to view #{field.name} in API" |
|
87 | assert_select "content[type=html]", { :text => /NewValue#{i}/, :count => 0 }, "User #{user.id} was able to view #{field.name} in API" | |
88 | end |
|
88 | end | |
89 | end |
|
89 | end | |
90 | end |
|
90 | end | |
91 |
|
91 | |||
92 | end |
|
92 | end | |
93 |
|
93 | |||
94 | def test_diff_for_description_change |
|
94 | def test_diff_for_description_change | |
95 | get :diff, :id => 3, :detail_id => 4 |
|
95 | get :diff, :id => 3, :detail_id => 4 | |
96 | assert_response :success |
|
96 | assert_response :success | |
97 | assert_template 'diff' |
|
97 | assert_template 'diff' | |
98 |
|
98 | |||
99 | assert_select 'span.diff_out', :text => /removed/ |
|
99 | assert_select 'span.diff_out', :text => /removed/ | |
100 | assert_select 'span.diff_in', :text => /added/ |
|
100 | assert_select 'span.diff_in', :text => /added/ | |
101 | end |
|
101 | end | |
102 |
|
102 | |||
103 | def test_diff_for_custom_field |
|
103 | def test_diff_for_custom_field | |
104 | field = IssueCustomField.create!(:name => "Long field", :field_format => 'text') |
|
104 | field = IssueCustomField.create!(:name => "Long field", :field_format => 'text') | |
105 | journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Notes', :user_id => 1) |
|
105 | journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Notes', :user_id => 1) | |
106 | detail = JournalDetail.create!(:journal => journal, :property => 'cf', :prop_key => field.id, |
|
106 | detail = JournalDetail.create!(:journal => journal, :property => 'cf', :prop_key => field.id, | |
107 | :old_value => 'Foo', :value => 'Bar') |
|
107 | :old_value => 'Foo', :value => 'Bar') | |
108 |
|
108 | |||
109 | get :diff, :id => journal.id, :detail_id => detail.id |
|
109 | get :diff, :id => journal.id, :detail_id => detail.id | |
110 | assert_response :success |
|
110 | assert_response :success | |
111 | assert_template 'diff' |
|
111 | assert_template 'diff' | |
112 |
|
112 | |||
113 | assert_select 'span.diff_out', :text => /Foo/ |
|
113 | assert_select 'span.diff_out', :text => /Foo/ | |
114 | assert_select 'span.diff_in', :text => /Bar/ |
|
114 | assert_select 'span.diff_in', :text => /Bar/ | |
115 | end |
|
115 | end | |
116 |
|
116 | |||
117 | def test_diff_for_custom_field_should_be_denied_if_custom_field_is_not_visible |
|
117 | def test_diff_for_custom_field_should_be_denied_if_custom_field_is_not_visible | |
118 | field = IssueCustomField.create!(:name => "Long field", :field_format => 'text', :visible => false, :role_ids => [1]) |
|
118 | field = IssueCustomField.create!(:name => "Long field", :field_format => 'text', :visible => false, :role_ids => [1]) | |
119 | journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Notes', :user_id => 1) |
|
119 | journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Notes', :user_id => 1) | |
120 | detail = JournalDetail.create!(:journal => journal, :property => 'cf', :prop_key => field.id, |
|
120 | detail = JournalDetail.create!(:journal => journal, :property => 'cf', :prop_key => field.id, | |
121 | :old_value => 'Foo', :value => 'Bar') |
|
121 | :old_value => 'Foo', :value => 'Bar') | |
122 |
|
122 | |||
123 | get :diff, :id => journal.id, :detail_id => detail.id |
|
123 | get :diff, :id => journal.id, :detail_id => detail.id | |
124 | assert_response 302 |
|
124 | assert_response 302 | |
125 | end |
|
125 | end | |
126 |
|
126 | |||
127 | def test_diff_should_default_to_description_diff |
|
127 | def test_diff_should_default_to_description_diff | |
128 | get :diff, :id => 3 |
|
128 | get :diff, :id => 3 | |
129 | assert_response :success |
|
129 | assert_response :success | |
130 | assert_template 'diff' |
|
130 | assert_template 'diff' | |
131 |
|
131 | |||
132 | assert_select 'span.diff_out', :text => /removed/ |
|
132 | assert_select 'span.diff_out', :text => /removed/ | |
133 | assert_select 'span.diff_in', :text => /added/ |
|
133 | assert_select 'span.diff_in', :text => /added/ | |
134 | end |
|
134 | end | |
135 |
|
135 | |||
136 | def test_reply_to_issue |
|
136 | def test_reply_to_issue | |
137 | @request.session[:user_id] = 2 |
|
137 | @request.session[:user_id] = 2 | |
138 | xhr :get, :new, :id => 6 |
|
138 | xhr :get, :new, :id => 6 | |
139 | assert_response :success |
|
139 | assert_response :success | |
140 | assert_template 'new' |
|
140 | assert_template 'new' | |
141 | assert_equal 'text/javascript', response.content_type |
|
141 | assert_equal 'text/javascript', response.content_type | |
142 | assert_include '> This is an issue', response.body |
|
142 | assert_include '> This is an issue', response.body | |
143 | end |
|
143 | end | |
144 |
|
144 | |||
145 | def test_reply_to_issue_without_permission |
|
145 | def test_reply_to_issue_without_permission | |
146 | @request.session[:user_id] = 7 |
|
146 | @request.session[:user_id] = 7 | |
147 | xhr :get, :new, :id => 6 |
|
147 | xhr :get, :new, :id => 6 | |
148 | assert_response 403 |
|
148 | assert_response 403 | |
149 | end |
|
149 | end | |
150 |
|
150 | |||
151 | def test_reply_to_note |
|
151 | def test_reply_to_note | |
152 | @request.session[:user_id] = 2 |
|
152 | @request.session[:user_id] = 2 | |
153 | xhr :get, :new, :id => 6, :journal_id => 4 |
|
153 | xhr :get, :new, :id => 6, :journal_id => 4 | |
154 | assert_response :success |
|
154 | assert_response :success | |
155 | assert_template 'new' |
|
155 | assert_template 'new' | |
156 | assert_equal 'text/javascript', response.content_type |
|
156 | assert_equal 'text/javascript', response.content_type | |
157 | assert_include '> A comment with a private version', response.body |
|
157 | assert_include '> A comment with a private version', response.body | |
158 | end |
|
158 | end | |
159 |
|
159 | |||
160 | def test_reply_to_private_note_should_fail_without_permission |
|
160 | def test_reply_to_private_note_should_fail_without_permission | |
161 | journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true) |
|
161 | journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true) | |
162 | @request.session[:user_id] = 2 |
|
162 | @request.session[:user_id] = 2 | |
163 |
|
163 | |||
164 | xhr :get, :new, :id => 2, :journal_id => journal.id |
|
164 | xhr :get, :new, :id => 2, :journal_id => journal.id | |
165 | assert_response :success |
|
165 | assert_response :success | |
166 | assert_template 'new' |
|
166 | assert_template 'new' | |
167 | assert_equal 'text/javascript', response.content_type |
|
167 | assert_equal 'text/javascript', response.content_type | |
168 | assert_include '> Privates notes', response.body |
|
168 | assert_include '> Privates notes', response.body | |
169 |
|
169 | |||
170 | Role.find(1).remove_permission! :view_private_notes |
|
170 | Role.find(1).remove_permission! :view_private_notes | |
171 | xhr :get, :new, :id => 2, :journal_id => journal.id |
|
171 | xhr :get, :new, :id => 2, :journal_id => journal.id | |
172 | assert_response 404 |
|
172 | assert_response 404 | |
173 | end |
|
173 | end | |
174 |
|
174 | |||
175 | def test_edit_xhr |
|
175 | def test_edit_xhr | |
176 | @request.session[:user_id] = 1 |
|
176 | @request.session[:user_id] = 1 | |
177 | xhr :get, :edit, :id => 2 |
|
177 | xhr :get, :edit, :id => 2 | |
178 | assert_response :success |
|
178 | assert_response :success | |
179 | assert_template 'edit' |
|
179 | assert_template 'edit' | |
180 | assert_equal 'text/javascript', response.content_type |
|
180 | assert_equal 'text/javascript', response.content_type | |
181 | assert_include 'textarea', response.body |
|
181 | assert_include 'textarea', response.body | |
182 | end |
|
182 | end | |
183 |
|
183 | |||
184 | def test_edit_private_note_should_fail_without_permission |
|
184 | def test_edit_private_note_should_fail_without_permission | |
185 | journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true) |
|
185 | journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true) | |
186 | @request.session[:user_id] = 2 |
|
186 | @request.session[:user_id] = 2 | |
187 | Role.find(1).add_permission! :edit_issue_notes |
|
187 | Role.find(1).add_permission! :edit_issue_notes | |
188 |
|
188 | |||
189 | xhr :get, :edit, :id => journal.id |
|
189 | xhr :get, :edit, :id => journal.id | |
190 | assert_response :success |
|
190 | assert_response :success | |
191 | assert_template 'edit' |
|
191 | assert_template 'edit' | |
192 | assert_equal 'text/javascript', response.content_type |
|
192 | assert_equal 'text/javascript', response.content_type | |
193 | assert_include 'textarea', response.body |
|
193 | assert_include 'textarea', response.body | |
194 |
|
194 | |||
195 | Role.find(1).remove_permission! :view_private_notes |
|
195 | Role.find(1).remove_permission! :view_private_notes | |
196 | xhr :get, :edit, :id => journal.id |
|
196 | xhr :get, :edit, :id => journal.id | |
197 | assert_response 404 |
|
197 | assert_response 404 | |
198 | end |
|
198 | end | |
199 |
|
199 | |||
200 | def test_update_xhr |
|
200 | def test_update_xhr | |
201 | @request.session[:user_id] = 1 |
|
201 | @request.session[:user_id] = 1 | |
202 | xhr :post, :update, :id => 2, :notes => 'Updated notes' |
|
202 | xhr :post, :update, :id => 2, :journal => {:notes => 'Updated notes'} | |
203 | assert_response :success |
|
203 | assert_response :success | |
204 | assert_template 'update' |
|
204 | assert_template 'update' | |
205 | assert_equal 'text/javascript', response.content_type |
|
205 | assert_equal 'text/javascript', response.content_type | |
206 | assert_equal 'Updated notes', Journal.find(2).notes |
|
206 | assert_equal 'Updated notes', Journal.find(2).notes | |
207 | assert_include 'journal-2-notes', response.body |
|
207 | assert_include 'journal-2-notes', response.body | |
208 | end |
|
208 | end | |
209 |
|
209 | |||
210 | def test_update_xhr_with_private_notes_checked |
|
210 | def test_update_xhr_with_private_notes_checked | |
211 | @request.session[:user_id] = 1 |
|
211 | @request.session[:user_id] = 1 | |
212 | xhr :post, :update, :id => 2, :private_notes => '1' |
|
212 | xhr :post, :update, :id => 2, :journal => {:private_notes => '1'} | |
213 | assert_response :success |
|
213 | assert_response :success | |
214 | assert_template 'update' |
|
214 | assert_template 'update' | |
215 | assert_equal 'text/javascript', response.content_type |
|
215 | assert_equal 'text/javascript', response.content_type | |
216 | assert_equal true, Journal.find(2).private_notes |
|
216 | assert_equal true, Journal.find(2).private_notes | |
217 | assert_include 'change-2', response.body |
|
217 | assert_include 'change-2', response.body | |
218 | assert_include 'journal-2-private_notes', response.body |
|
218 | assert_include 'journal-2-private_notes', response.body | |
219 | end |
|
219 | end | |
220 |
|
220 | |||
221 | def test_update_xhr_with_private_notes_unchecked |
|
221 | def test_update_xhr_with_private_notes_unchecked | |
222 | Journal.find(2).update_attributes(:private_notes => true) |
|
222 | Journal.find(2).update_attributes(:private_notes => true) | |
223 | @request.session[:user_id] = 1 |
|
223 | @request.session[:user_id] = 1 | |
224 | xhr :post, :update, :id => 2 |
|
224 | xhr :post, :update, :id => 2, :journal => {:private_notes => '0'} | |
225 | assert_response :success |
|
225 | assert_response :success | |
226 | assert_template 'update' |
|
226 | assert_template 'update' | |
227 | assert_equal 'text/javascript', response.content_type |
|
227 | assert_equal 'text/javascript', response.content_type | |
228 | assert_equal false, Journal.find(2).private_notes |
|
228 | assert_equal false, Journal.find(2).private_notes | |
229 | assert_include 'change-2', response.body |
|
229 | assert_include 'change-2', response.body | |
230 | assert_include 'journal-2-private_notes', response.body |
|
230 | assert_include 'journal-2-private_notes', response.body | |
231 | end |
|
231 | end | |
232 |
|
232 | |||
233 |
def test_update_xhr_with |
|
233 | def test_update_xhr_without_set_private_notes_permission_should_ignore_private_notes | |
234 | @request.session[:user_id] = 2 |
|
234 | @request.session[:user_id] = 2 | |
235 | Role.find(1).add_permission! :edit_issue_notes |
|
235 | Role.find(1).add_permission! :edit_issue_notes | |
236 | Role.find(1).add_permission! :view_private_notes |
|
236 | Role.find(1).add_permission! :view_private_notes | |
237 | Role.find(1).remove_permission! :set_notes_private |
|
237 | Role.find(1).remove_permission! :set_notes_private | |
238 |
|
238 | |||
239 | xhr :post, :update, :id => 2, :private_notes => '1' |
|
239 | xhr :post, :update, :id => 2, :journal => {:private_notes => '1'} | |
240 |
assert_response |
|
240 | assert_response :success | |
|
241 | assert_equal false, Journal.find(2).private_notes | |||
241 | end |
|
242 | end | |
242 |
|
243 | |||
243 | def test_update_xhr_with_empty_notes_should_delete_the_journal |
|
244 | def test_update_xhr_with_empty_notes_should_delete_the_journal | |
244 | @request.session[:user_id] = 1 |
|
245 | @request.session[:user_id] = 1 | |
245 | assert_difference 'Journal.count', -1 do |
|
246 | assert_difference 'Journal.count', -1 do | |
246 | xhr :post, :update, :id => 2, :notes => '' |
|
247 | xhr :post, :update, :id => 2, :journal => {:notes => ''} | |
247 | assert_response :success |
|
248 | assert_response :success | |
248 | assert_template 'update' |
|
249 | assert_template 'update' | |
249 | assert_equal 'text/javascript', response.content_type |
|
250 | assert_equal 'text/javascript', response.content_type | |
250 | end |
|
251 | end | |
251 | assert_nil Journal.find_by_id(2) |
|
252 | assert_nil Journal.find_by_id(2) | |
252 | assert_include 'change-2', response.body |
|
253 | assert_include 'change-2', response.body | |
253 | end |
|
254 | end | |
254 | end |
|
255 | end |
General Comments 0
You need to be logged in to leave comments.
Login now