##// END OF EJS Templates
AttachmentsController now handles attachments deletion....
Jean-Philippe Lang -
r2114:5d2899ee1b3e
parent child
Show More
@@ -0,0 +1,2
1 require File.dirname(__FILE__) + '/lib/acts_as_attachable'
2 ActiveRecord::Base.send(:include, Redmine::Acts::Attachable)
@@ -0,0 +1,57
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 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 Acts
20 module Attachable
21 def self.included(base)
22 base.extend ClassMethods
23 end
24
25 module ClassMethods
26 def acts_as_attachable(options = {})
27 cattr_accessor :attachable_options
28 self.attachable_options = {}
29 attachable_options[:view_permission] = options.delete(:view_permission) || "view_#{self.name.pluralize.underscore}".to_sym
30 attachable_options[:delete_permission] = options.delete(:delete_permission) || "edit_#{self.name.pluralize.underscore}".to_sym
31
32 has_many :attachments, options.merge(:as => :container,
33 :order => "#{Attachment.table_name}.created_on",
34 :dependent => :destroy)
35 send :include, Redmine::Acts::Attachable::InstanceMethods
36 end
37 end
38
39 module InstanceMethods
40 def self.included(base)
41 base.extend ClassMethods
42 end
43
44 def attachments_visible?(user=User.current)
45 user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
46 end
47
48 def attachments_deletable?(user=User.current)
49 user.allowed_to?(self.class.attachable_options[:delete_permission], self.project)
50 end
51
52 module ClassMethods
53 end
54 end
55 end
56 end
57 end
@@ -1,55 +1,72
1 # redMine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 AttachmentsController < ApplicationController
18 class AttachmentsController < ApplicationController
19 before_filter :find_project
19 before_filter :find_project
20
20 before_filter :read_authorize, :except => :destroy
21 before_filter :delete_authorize, :only => :destroy
22
23 verify :method => :post, :only => :destroy
24
21 def show
25 def show
22 if @attachment.is_diff?
26 if @attachment.is_diff?
23 @diff = File.new(@attachment.diskfile, "rb").read
27 @diff = File.new(@attachment.diskfile, "rb").read
24 render :action => 'diff'
28 render :action => 'diff'
25 elsif @attachment.is_text?
29 elsif @attachment.is_text?
26 @content = File.new(@attachment.diskfile, "rb").read
30 @content = File.new(@attachment.diskfile, "rb").read
27 render :action => 'file'
31 render :action => 'file'
28 elsif
32 elsif
29 download
33 download
30 end
34 end
31 end
35 end
32
36
33 def download
37 def download
34 @attachment.increment_download if @attachment.container.is_a?(Version)
38 @attachment.increment_download if @attachment.container.is_a?(Version)
35
39
36 # images are sent inline
40 # images are sent inline
37 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
41 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
38 :type => @attachment.content_type,
42 :type => @attachment.content_type,
39 :disposition => (@attachment.image? ? 'inline' : 'attachment')
43 :disposition => (@attachment.image? ? 'inline' : 'attachment')
44
40 end
45 end
41
46
47 def destroy
48 # Make sure association callbacks are called
49 @attachment.container.attachments.delete(@attachment)
50 redirect_to :back
51 rescue ::ActionController::RedirectBackError
52 redirect_to :controller => 'projects', :action => 'show', :id => @project
53 end
54
42 private
55 private
43 def find_project
56 def find_project
44 @attachment = Attachment.find(params[:id])
57 @attachment = Attachment.find(params[:id])
45 # Show 404 if the filename in the url is wrong
58 # Show 404 if the filename in the url is wrong
46 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
59 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
47
48 @project = @attachment.project
60 @project = @attachment.project
49 permission = @attachment.container.is_a?(Version) ? :view_files : "view_#{@attachment.container.class.name.underscore.pluralize}".to_sym
50 allowed = User.current.allowed_to?(permission, @project)
51 allowed ? true : (User.current.logged? ? render_403 : require_login)
52 rescue ActiveRecord::RecordNotFound
61 rescue ActiveRecord::RecordNotFound
53 render_404
62 render_404
54 end
63 end
64
65 def read_authorize
66 @attachment.visible? ? true : deny_access
67 end
68
69 def delete_authorize
70 @attachment.deletable? ? true : deny_access
71 end
55 end
72 end
@@ -1,92 +1,87
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 DocumentsController < ApplicationController
18 class DocumentsController < ApplicationController
19 before_filter :find_project, :only => [:index, :new]
19 before_filter :find_project, :only => [:index, :new]
20 before_filter :find_document, :except => [:index, :new]
20 before_filter :find_document, :except => [:index, :new]
21 before_filter :authorize
21 before_filter :authorize
22
22
23 helper :attachments
23 helper :attachments
24
24
25 def index
25 def index
26 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
26 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
27 documents = @project.documents.find :all, :include => [:attachments, :category]
27 documents = @project.documents.find :all, :include => [:attachments, :category]
28 case @sort_by
28 case @sort_by
29 when 'date'
29 when 'date'
30 @grouped = documents.group_by {|d| d.created_on.to_date }
30 @grouped = documents.group_by {|d| d.created_on.to_date }
31 when 'title'
31 when 'title'
32 @grouped = documents.group_by {|d| d.title.first.upcase}
32 @grouped = documents.group_by {|d| d.title.first.upcase}
33 when 'author'
33 when 'author'
34 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
34 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
35 else
35 else
36 @grouped = documents.group_by(&:category)
36 @grouped = documents.group_by(&:category)
37 end
37 end
38 render :layout => false if request.xhr?
38 render :layout => false if request.xhr?
39 end
39 end
40
40
41 def show
41 def show
42 @attachments = @document.attachments.find(:all, :order => "created_on DESC")
42 @attachments = @document.attachments.find(:all, :order => "created_on DESC")
43 end
43 end
44
44
45 def new
45 def new
46 @document = @project.documents.build(params[:document])
46 @document = @project.documents.build(params[:document])
47 if request.post? and @document.save
47 if request.post? and @document.save
48 attach_files(@document, params[:attachments])
48 attach_files(@document, params[:attachments])
49 flash[:notice] = l(:notice_successful_create)
49 flash[:notice] = l(:notice_successful_create)
50 Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added')
50 Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added')
51 redirect_to :action => 'index', :project_id => @project
51 redirect_to :action => 'index', :project_id => @project
52 end
52 end
53 end
53 end
54
54
55 def edit
55 def edit
56 @categories = Enumeration::get_values('DCAT')
56 @categories = Enumeration::get_values('DCAT')
57 if request.post? and @document.update_attributes(params[:document])
57 if request.post? and @document.update_attributes(params[:document])
58 flash[:notice] = l(:notice_successful_update)
58 flash[:notice] = l(:notice_successful_update)
59 redirect_to :action => 'show', :id => @document
59 redirect_to :action => 'show', :id => @document
60 end
60 end
61 end
61 end
62
62
63 def destroy
63 def destroy
64 @document.destroy
64 @document.destroy
65 redirect_to :controller => 'documents', :action => 'index', :project_id => @project
65 redirect_to :controller => 'documents', :action => 'index', :project_id => @project
66 end
66 end
67
67
68 def add_attachment
68 def add_attachment
69 attachments = attach_files(@document, params[:attachments])
69 attachments = attach_files(@document, params[:attachments])
70 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added')
70 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added')
71 redirect_to :action => 'show', :id => @document
71 redirect_to :action => 'show', :id => @document
72 end
72 end
73
74 def destroy_attachment
75 @document.attachments.find(params[:attachment_id]).destroy
76 redirect_to :action => 'show', :id => @document
77 end
78
73
79 private
74 private
80 def find_project
75 def find_project
81 @project = Project.find(params[:project_id])
76 @project = Project.find(params[:project_id])
82 rescue ActiveRecord::RecordNotFound
77 rescue ActiveRecord::RecordNotFound
83 render_404
78 render_404
84 end
79 end
85
80
86 def find_document
81 def find_document
87 @document = Document.find(params[:id])
82 @document = Document.find(params[:id])
88 @project = @document.project
83 @project = @document.project
89 rescue ActiveRecord::RecordNotFound
84 rescue ActiveRecord::RecordNotFound
90 render_404
85 render_404
91 end
86 end
92 end
87 end
@@ -1,493 +1,482
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => :new
19 menu_item :new_issue, :only => :new
20
20
21 before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment]
21 before_filter :find_issue, :only => [:show, :edit, :reply]
22 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
22 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_project, :only => [:new, :update_form, :preview]
23 before_filter :find_project, :only => [:new, :update_form, :preview]
24 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
24 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
25 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
25 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 accept_key_auth :index, :changes
26 accept_key_auth :index, :changes
27
27
28 helper :journals
28 helper :journals
29 helper :projects
29 helper :projects
30 include ProjectsHelper
30 include ProjectsHelper
31 helper :custom_fields
31 helper :custom_fields
32 include CustomFieldsHelper
32 include CustomFieldsHelper
33 helper :ifpdf
33 helper :ifpdf
34 include IfpdfHelper
34 include IfpdfHelper
35 helper :issue_relations
35 helper :issue_relations
36 include IssueRelationsHelper
36 include IssueRelationsHelper
37 helper :watchers
37 helper :watchers
38 include WatchersHelper
38 include WatchersHelper
39 helper :attachments
39 helper :attachments
40 include AttachmentsHelper
40 include AttachmentsHelper
41 helper :queries
41 helper :queries
42 helper :sort
42 helper :sort
43 include SortHelper
43 include SortHelper
44 include IssuesHelper
44 include IssuesHelper
45 helper :timelog
45 helper :timelog
46
46
47 def index
47 def index
48 sort_init "#{Issue.table_name}.id", "desc"
48 sort_init "#{Issue.table_name}.id", "desc"
49 sort_update
49 sort_update
50 retrieve_query
50 retrieve_query
51 if @query.valid?
51 if @query.valid?
52 limit = per_page_option
52 limit = per_page_option
53 respond_to do |format|
53 respond_to do |format|
54 format.html { }
54 format.html { }
55 format.atom { }
55 format.atom { }
56 format.csv { limit = Setting.issues_export_limit.to_i }
56 format.csv { limit = Setting.issues_export_limit.to_i }
57 format.pdf { limit = Setting.issues_export_limit.to_i }
57 format.pdf { limit = Setting.issues_export_limit.to_i }
58 end
58 end
59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61 @issues = Issue.find :all, :order => sort_clause,
61 @issues = Issue.find :all, :order => sort_clause,
62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63 :conditions => @query.statement,
63 :conditions => @query.statement,
64 :limit => limit,
64 :limit => limit,
65 :offset => @issue_pages.current.offset
65 :offset => @issue_pages.current.offset
66 respond_to do |format|
66 respond_to do |format|
67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
68 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
68 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
70 format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
70 format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
71 end
71 end
72 else
72 else
73 # Send html if the query is not valid
73 # Send html if the query is not valid
74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
75 end
75 end
76 rescue ActiveRecord::RecordNotFound
76 rescue ActiveRecord::RecordNotFound
77 render_404
77 render_404
78 end
78 end
79
79
80 def changes
80 def changes
81 sort_init "#{Issue.table_name}.id", "desc"
81 sort_init "#{Issue.table_name}.id", "desc"
82 sort_update
82 sort_update
83 retrieve_query
83 retrieve_query
84 if @query.valid?
84 if @query.valid?
85 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
85 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
86 :conditions => @query.statement,
86 :conditions => @query.statement,
87 :limit => 25,
87 :limit => 25,
88 :order => "#{Journal.table_name}.created_on DESC"
88 :order => "#{Journal.table_name}.created_on DESC"
89 end
89 end
90 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
90 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
91 render :layout => false, :content_type => 'application/atom+xml'
91 render :layout => false, :content_type => 'application/atom+xml'
92 rescue ActiveRecord::RecordNotFound
92 rescue ActiveRecord::RecordNotFound
93 render_404
93 render_404
94 end
94 end
95
95
96 def show
96 def show
97 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
97 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
98 @journals.each_with_index {|j,i| j.indice = i+1}
98 @journals.each_with_index {|j,i| j.indice = i+1}
99 @journals.reverse! if User.current.wants_comments_in_reverse_order?
99 @journals.reverse! if User.current.wants_comments_in_reverse_order?
100 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
100 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
101 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
101 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
102 @priorities = Enumeration::get_values('IPRI')
102 @priorities = Enumeration::get_values('IPRI')
103 @time_entry = TimeEntry.new
103 @time_entry = TimeEntry.new
104 respond_to do |format|
104 respond_to do |format|
105 format.html { render :template => 'issues/show.rhtml' }
105 format.html { render :template => 'issues/show.rhtml' }
106 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
106 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
107 format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
107 format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
108 end
108 end
109 end
109 end
110
110
111 # Add a new issue
111 # Add a new issue
112 # The new issue will be created from an existing one if copy_from parameter is given
112 # The new issue will be created from an existing one if copy_from parameter is given
113 def new
113 def new
114 @issue = Issue.new
114 @issue = Issue.new
115 @issue.copy_from(params[:copy_from]) if params[:copy_from]
115 @issue.copy_from(params[:copy_from]) if params[:copy_from]
116 @issue.project = @project
116 @issue.project = @project
117 # Tracker must be set before custom field values
117 # Tracker must be set before custom field values
118 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
118 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
119 if @issue.tracker.nil?
119 if @issue.tracker.nil?
120 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
120 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
121 render :nothing => true, :layout => true
121 render :nothing => true, :layout => true
122 return
122 return
123 end
123 end
124 @issue.attributes = params[:issue]
124 @issue.attributes = params[:issue]
125 @issue.author = User.current
125 @issue.author = User.current
126
126
127 default_status = IssueStatus.default
127 default_status = IssueStatus.default
128 unless default_status
128 unless default_status
129 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
129 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
130 render :nothing => true, :layout => true
130 render :nothing => true, :layout => true
131 return
131 return
132 end
132 end
133 @issue.status = default_status
133 @issue.status = default_status
134 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
134 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
135
135
136 if request.get? || request.xhr?
136 if request.get? || request.xhr?
137 @issue.start_date ||= Date.today
137 @issue.start_date ||= Date.today
138 else
138 else
139 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
139 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
140 # Check that the user is allowed to apply the requested status
140 # Check that the user is allowed to apply the requested status
141 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
141 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
142 if @issue.save
142 if @issue.save
143 attach_files(@issue, params[:attachments])
143 attach_files(@issue, params[:attachments])
144 flash[:notice] = l(:notice_successful_create)
144 flash[:notice] = l(:notice_successful_create)
145 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
145 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
146 redirect_to :controller => 'issues', :action => 'show', :id => @issue
146 redirect_to :controller => 'issues', :action => 'show', :id => @issue
147 return
147 return
148 end
148 end
149 end
149 end
150 @priorities = Enumeration::get_values('IPRI')
150 @priorities = Enumeration::get_values('IPRI')
151 render :layout => !request.xhr?
151 render :layout => !request.xhr?
152 end
152 end
153
153
154 # Attributes that can be updated on workflow transition (without :edit permission)
154 # Attributes that can be updated on workflow transition (without :edit permission)
155 # TODO: make it configurable (at least per role)
155 # TODO: make it configurable (at least per role)
156 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
156 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
157
157
158 def edit
158 def edit
159 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
159 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
160 @priorities = Enumeration::get_values('IPRI')
160 @priorities = Enumeration::get_values('IPRI')
161 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
161 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
162 @time_entry = TimeEntry.new
162 @time_entry = TimeEntry.new
163
163
164 @notes = params[:notes]
164 @notes = params[:notes]
165 journal = @issue.init_journal(User.current, @notes)
165 journal = @issue.init_journal(User.current, @notes)
166 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
166 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
167 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
167 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
168 attrs = params[:issue].dup
168 attrs = params[:issue].dup
169 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
169 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
170 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
170 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
171 @issue.attributes = attrs
171 @issue.attributes = attrs
172 end
172 end
173
173
174 if request.post?
174 if request.post?
175 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
175 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
176 @time_entry.attributes = params[:time_entry]
176 @time_entry.attributes = params[:time_entry]
177 attachments = attach_files(@issue, params[:attachments])
177 attachments = attach_files(@issue, params[:attachments])
178 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
178 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
179
179
180 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
180 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
181
181
182 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
182 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
183 # Log spend time
183 # Log spend time
184 if current_role.allowed_to?(:log_time)
184 if current_role.allowed_to?(:log_time)
185 @time_entry.save
185 @time_entry.save
186 end
186 end
187 if !journal.new_record?
187 if !journal.new_record?
188 # Only send notification if something was actually changed
188 # Only send notification if something was actually changed
189 flash[:notice] = l(:notice_successful_update)
189 flash[:notice] = l(:notice_successful_update)
190 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
190 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
191 end
191 end
192 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
192 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
193 end
193 end
194 end
194 end
195 rescue ActiveRecord::StaleObjectError
195 rescue ActiveRecord::StaleObjectError
196 # Optimistic locking exception
196 # Optimistic locking exception
197 flash.now[:error] = l(:notice_locking_conflict)
197 flash.now[:error] = l(:notice_locking_conflict)
198 end
198 end
199
199
200 def reply
200 def reply
201 journal = Journal.find(params[:journal_id]) if params[:journal_id]
201 journal = Journal.find(params[:journal_id]) if params[:journal_id]
202 if journal
202 if journal
203 user = journal.user
203 user = journal.user
204 text = journal.notes
204 text = journal.notes
205 else
205 else
206 user = @issue.author
206 user = @issue.author
207 text = @issue.description
207 text = @issue.description
208 end
208 end
209 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
209 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
210 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
210 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
211 render(:update) { |page|
211 render(:update) { |page|
212 page.<< "$('notes').value = \"#{content}\";"
212 page.<< "$('notes').value = \"#{content}\";"
213 page.show 'update'
213 page.show 'update'
214 page << "Form.Element.focus('notes');"
214 page << "Form.Element.focus('notes');"
215 page << "Element.scrollTo('update');"
215 page << "Element.scrollTo('update');"
216 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
216 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
217 }
217 }
218 end
218 end
219
219
220 # Bulk edit a set of issues
220 # Bulk edit a set of issues
221 def bulk_edit
221 def bulk_edit
222 if request.post?
222 if request.post?
223 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
223 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
224 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
224 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
225 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
225 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
226 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
226 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
227 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
227 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
228
228
229 unsaved_issue_ids = []
229 unsaved_issue_ids = []
230 @issues.each do |issue|
230 @issues.each do |issue|
231 journal = issue.init_journal(User.current, params[:notes])
231 journal = issue.init_journal(User.current, params[:notes])
232 issue.priority = priority if priority
232 issue.priority = priority if priority
233 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
233 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
234 issue.category = category if category || params[:category_id] == 'none'
234 issue.category = category if category || params[:category_id] == 'none'
235 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
235 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
236 issue.start_date = params[:start_date] unless params[:start_date].blank?
236 issue.start_date = params[:start_date] unless params[:start_date].blank?
237 issue.due_date = params[:due_date] unless params[:due_date].blank?
237 issue.due_date = params[:due_date] unless params[:due_date].blank?
238 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
238 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
239 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
239 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
240 # Don't save any change to the issue if the user is not authorized to apply the requested status
240 # Don't save any change to the issue if the user is not authorized to apply the requested status
241 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
241 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
242 # Send notification for each issue (if changed)
242 # Send notification for each issue (if changed)
243 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
243 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
244 else
244 else
245 # Keep unsaved issue ids to display them in flash error
245 # Keep unsaved issue ids to display them in flash error
246 unsaved_issue_ids << issue.id
246 unsaved_issue_ids << issue.id
247 end
247 end
248 end
248 end
249 if unsaved_issue_ids.empty?
249 if unsaved_issue_ids.empty?
250 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
250 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
251 else
251 else
252 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
252 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
253 end
253 end
254 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
254 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
255 return
255 return
256 end
256 end
257 # Find potential statuses the user could be allowed to switch issues to
257 # Find potential statuses the user could be allowed to switch issues to
258 @available_statuses = Workflow.find(:all, :include => :new_status,
258 @available_statuses = Workflow.find(:all, :include => :new_status,
259 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
259 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
260 end
260 end
261
261
262 def move
262 def move
263 @allowed_projects = []
263 @allowed_projects = []
264 # find projects to which the user is allowed to move the issue
264 # find projects to which the user is allowed to move the issue
265 if User.current.admin?
265 if User.current.admin?
266 # admin is allowed to move issues to any active (visible) project
266 # admin is allowed to move issues to any active (visible) project
267 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
267 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
268 else
268 else
269 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
269 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
270 end
270 end
271 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
271 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
272 @target_project ||= @project
272 @target_project ||= @project
273 @trackers = @target_project.trackers
273 @trackers = @target_project.trackers
274 if request.post?
274 if request.post?
275 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
275 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
276 unsaved_issue_ids = []
276 unsaved_issue_ids = []
277 @issues.each do |issue|
277 @issues.each do |issue|
278 issue.init_journal(User.current)
278 issue.init_journal(User.current)
279 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
279 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
280 end
280 end
281 if unsaved_issue_ids.empty?
281 if unsaved_issue_ids.empty?
282 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
282 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
283 else
283 else
284 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
284 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
285 end
285 end
286 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
286 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
287 return
287 return
288 end
288 end
289 render :layout => false if request.xhr?
289 render :layout => false if request.xhr?
290 end
290 end
291
291
292 def destroy
292 def destroy
293 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
293 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
294 if @hours > 0
294 if @hours > 0
295 case params[:todo]
295 case params[:todo]
296 when 'destroy'
296 when 'destroy'
297 # nothing to do
297 # nothing to do
298 when 'nullify'
298 when 'nullify'
299 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
299 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
300 when 'reassign'
300 when 'reassign'
301 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
301 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
302 if reassign_to.nil?
302 if reassign_to.nil?
303 flash.now[:error] = l(:error_issue_not_found_in_project)
303 flash.now[:error] = l(:error_issue_not_found_in_project)
304 return
304 return
305 else
305 else
306 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
306 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
307 end
307 end
308 else
308 else
309 # display the destroy form
309 # display the destroy form
310 return
310 return
311 end
311 end
312 end
312 end
313 @issues.each(&:destroy)
313 @issues.each(&:destroy)
314 redirect_to :action => 'index', :project_id => @project
314 redirect_to :action => 'index', :project_id => @project
315 end
315 end
316
317 def destroy_attachment
318 a = @issue.attachments.find(params[:attachment_id])
319 a.destroy
320 journal = @issue.init_journal(User.current)
321 journal.details << JournalDetail.new(:property => 'attachment',
322 :prop_key => a.id,
323 :old_value => a.filename)
324 journal.save
325 redirect_to :action => 'show', :id => @issue
326 end
327
316
328 def gantt
317 def gantt
329 @gantt = Redmine::Helpers::Gantt.new(params)
318 @gantt = Redmine::Helpers::Gantt.new(params)
330 retrieve_query
319 retrieve_query
331 if @query.valid?
320 if @query.valid?
332 events = []
321 events = []
333 # Issues that have start and due dates
322 # Issues that have start and due dates
334 events += Issue.find(:all,
323 events += Issue.find(:all,
335 :order => "start_date, due_date",
324 :order => "start_date, due_date",
336 :include => [:tracker, :status, :assigned_to, :priority, :project],
325 :include => [:tracker, :status, :assigned_to, :priority, :project],
337 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
326 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
338 )
327 )
339 # Issues that don't have a due date but that are assigned to a version with a date
328 # Issues that don't have a due date but that are assigned to a version with a date
340 events += Issue.find(:all,
329 events += Issue.find(:all,
341 :order => "start_date, effective_date",
330 :order => "start_date, effective_date",
342 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
331 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
343 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
332 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
344 )
333 )
345 # Versions
334 # Versions
346 events += Version.find(:all, :include => :project,
335 events += Version.find(:all, :include => :project,
347 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
336 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
348
337
349 @gantt.events = events
338 @gantt.events = events
350 end
339 end
351
340
352 respond_to do |format|
341 respond_to do |format|
353 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
342 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
354 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png") } if @gantt.respond_to?('to_image')
343 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png") } if @gantt.respond_to?('to_image')
355 format.pdf { send_data(render(:template => "issues/gantt.rfpdf", :layout => false), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
344 format.pdf { send_data(render(:template => "issues/gantt.rfpdf", :layout => false), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
356 end
345 end
357 end
346 end
358
347
359 def calendar
348 def calendar
360 if params[:year] and params[:year].to_i > 1900
349 if params[:year] and params[:year].to_i > 1900
361 @year = params[:year].to_i
350 @year = params[:year].to_i
362 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
351 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
363 @month = params[:month].to_i
352 @month = params[:month].to_i
364 end
353 end
365 end
354 end
366 @year ||= Date.today.year
355 @year ||= Date.today.year
367 @month ||= Date.today.month
356 @month ||= Date.today.month
368
357
369 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
358 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
370 retrieve_query
359 retrieve_query
371 if @query.valid?
360 if @query.valid?
372 events = []
361 events = []
373 events += Issue.find(:all,
362 events += Issue.find(:all,
374 :include => [:tracker, :status, :assigned_to, :priority, :project],
363 :include => [:tracker, :status, :assigned_to, :priority, :project],
375 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
364 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
376 )
365 )
377 events += Version.find(:all, :include => :project,
366 events += Version.find(:all, :include => :project,
378 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
367 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
379
368
380 @calendar.events = events
369 @calendar.events = events
381 end
370 end
382
371
383 render :layout => false if request.xhr?
372 render :layout => false if request.xhr?
384 end
373 end
385
374
386 def context_menu
375 def context_menu
387 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
376 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
388 if (@issues.size == 1)
377 if (@issues.size == 1)
389 @issue = @issues.first
378 @issue = @issues.first
390 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
379 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
391 end
380 end
392 projects = @issues.collect(&:project).compact.uniq
381 projects = @issues.collect(&:project).compact.uniq
393 @project = projects.first if projects.size == 1
382 @project = projects.first if projects.size == 1
394
383
395 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
384 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
396 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
385 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
397 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
386 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
398 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
387 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
399 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
388 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
400 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
389 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
401 }
390 }
402 if @project
391 if @project
403 @assignables = @project.assignable_users
392 @assignables = @project.assignable_users
404 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
393 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
405 end
394 end
406
395
407 @priorities = Enumeration.get_values('IPRI').reverse
396 @priorities = Enumeration.get_values('IPRI').reverse
408 @statuses = IssueStatus.find(:all, :order => 'position')
397 @statuses = IssueStatus.find(:all, :order => 'position')
409 @back = request.env['HTTP_REFERER']
398 @back = request.env['HTTP_REFERER']
410
399
411 render :layout => false
400 render :layout => false
412 end
401 end
413
402
414 def update_form
403 def update_form
415 @issue = Issue.new(params[:issue])
404 @issue = Issue.new(params[:issue])
416 render :action => :new, :layout => false
405 render :action => :new, :layout => false
417 end
406 end
418
407
419 def preview
408 def preview
420 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
409 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
421 @attachements = @issue.attachments if @issue
410 @attachements = @issue.attachments if @issue
422 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
411 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
423 render :partial => 'common/preview'
412 render :partial => 'common/preview'
424 end
413 end
425
414
426 private
415 private
427 def find_issue
416 def find_issue
428 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
417 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
429 @project = @issue.project
418 @project = @issue.project
430 rescue ActiveRecord::RecordNotFound
419 rescue ActiveRecord::RecordNotFound
431 render_404
420 render_404
432 end
421 end
433
422
434 # Filter for bulk operations
423 # Filter for bulk operations
435 def find_issues
424 def find_issues
436 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
425 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
437 raise ActiveRecord::RecordNotFound if @issues.empty?
426 raise ActiveRecord::RecordNotFound if @issues.empty?
438 projects = @issues.collect(&:project).compact.uniq
427 projects = @issues.collect(&:project).compact.uniq
439 if projects.size == 1
428 if projects.size == 1
440 @project = projects.first
429 @project = projects.first
441 else
430 else
442 # TODO: let users bulk edit/move/destroy issues from different projects
431 # TODO: let users bulk edit/move/destroy issues from different projects
443 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
432 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
444 end
433 end
445 rescue ActiveRecord::RecordNotFound
434 rescue ActiveRecord::RecordNotFound
446 render_404
435 render_404
447 end
436 end
448
437
449 def find_project
438 def find_project
450 @project = Project.find(params[:project_id])
439 @project = Project.find(params[:project_id])
451 rescue ActiveRecord::RecordNotFound
440 rescue ActiveRecord::RecordNotFound
452 render_404
441 render_404
453 end
442 end
454
443
455 def find_optional_project
444 def find_optional_project
456 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
445 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
457 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
446 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
458 allowed ? true : deny_access
447 allowed ? true : deny_access
459 rescue ActiveRecord::RecordNotFound
448 rescue ActiveRecord::RecordNotFound
460 render_404
449 render_404
461 end
450 end
462
451
463 # Retrieve query from session or build a new query
452 # Retrieve query from session or build a new query
464 def retrieve_query
453 def retrieve_query
465 if !params[:query_id].blank?
454 if !params[:query_id].blank?
466 cond = "project_id IS NULL"
455 cond = "project_id IS NULL"
467 cond << " OR project_id = #{@project.id}" if @project
456 cond << " OR project_id = #{@project.id}" if @project
468 @query = Query.find(params[:query_id], :conditions => cond)
457 @query = Query.find(params[:query_id], :conditions => cond)
469 @query.project = @project
458 @query.project = @project
470 session[:query] = {:id => @query.id, :project_id => @query.project_id}
459 session[:query] = {:id => @query.id, :project_id => @query.project_id}
471 else
460 else
472 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
461 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
473 # Give it a name, required to be valid
462 # Give it a name, required to be valid
474 @query = Query.new(:name => "_")
463 @query = Query.new(:name => "_")
475 @query.project = @project
464 @query.project = @project
476 if params[:fields] and params[:fields].is_a? Array
465 if params[:fields] and params[:fields].is_a? Array
477 params[:fields].each do |field|
466 params[:fields].each do |field|
478 @query.add_filter(field, params[:operators][field], params[:values][field])
467 @query.add_filter(field, params[:operators][field], params[:values][field])
479 end
468 end
480 else
469 else
481 @query.available_filters.keys.each do |field|
470 @query.available_filters.keys.each do |field|
482 @query.add_short_filter(field, params[field]) if params[field]
471 @query.add_short_filter(field, params[field]) if params[field]
483 end
472 end
484 end
473 end
485 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
474 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
486 else
475 else
487 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
476 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
488 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
477 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
489 @query.project = @project
478 @query.project = @project
490 end
479 end
491 end
480 end
492 end
481 end
493 end
482 end
@@ -1,60 +1,54
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 VersionsController < ApplicationController
18 class VersionsController < ApplicationController
19 menu_item :roadmap
19 menu_item :roadmap
20 before_filter :find_project, :authorize
20 before_filter :find_project, :authorize
21
21
22 def show
22 def show
23 end
23 end
24
24
25 def edit
25 def edit
26 if request.post? and @version.update_attributes(params[:version])
26 if request.post? and @version.update_attributes(params[:version])
27 flash[:notice] = l(:notice_successful_update)
27 flash[:notice] = l(:notice_successful_update)
28 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
28 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
29 end
29 end
30 end
30 end
31
31
32 def destroy
32 def destroy
33 @version.destroy
33 @version.destroy
34 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
34 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
35 rescue
35 rescue
36 flash[:error] = l(:notice_unable_delete_version)
36 flash[:error] = l(:notice_unable_delete_version)
37 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
37 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
38 end
38 end
39
39
40 def destroy_file
41 @version.attachments.find(params[:attachment_id]).destroy
42 flash[:notice] = l(:notice_successful_delete)
43 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
44 end
45
46 def status_by
40 def status_by
47 respond_to do |format|
41 respond_to do |format|
48 format.html { render :action => 'show' }
42 format.html { render :action => 'show' }
49 format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
43 format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
50 end
44 end
51 end
45 end
52
46
53 private
47 private
54 def find_project
48 def find_project
55 @version = Version.find(params[:id])
49 @version = Version.find(params[:id])
56 @project = @version.project
50 @project = @version.project
57 rescue ActiveRecord::RecordNotFound
51 rescue ActiveRecord::RecordNotFound
58 render_404
52 render_404
59 end
53 end
60 end
54 end
@@ -1,218 +1,211
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 'diff'
18 require 'diff'
19
19
20 class WikiController < ApplicationController
20 class WikiController < ApplicationController
21 before_filter :find_wiki, :authorize
21 before_filter :find_wiki, :authorize
22
22
23 verify :method => :post, :only => [:destroy, :destroy_attachment, :protect], :redirect_to => { :action => :index }
23 verify :method => :post, :only => [:destroy, :protect], :redirect_to => { :action => :index }
24
24
25 helper :attachments
25 helper :attachments
26 include AttachmentsHelper
26 include AttachmentsHelper
27
27
28 # display a page (in editing mode if it doesn't exist)
28 # display a page (in editing mode if it doesn't exist)
29 def index
29 def index
30 page_title = params[:page]
30 page_title = params[:page]
31 @page = @wiki.find_or_new_page(page_title)
31 @page = @wiki.find_or_new_page(page_title)
32 if @page.new_record?
32 if @page.new_record?
33 if User.current.allowed_to?(:edit_wiki_pages, @project)
33 if User.current.allowed_to?(:edit_wiki_pages, @project)
34 edit
34 edit
35 render :action => 'edit'
35 render :action => 'edit'
36 else
36 else
37 render_404
37 render_404
38 end
38 end
39 return
39 return
40 end
40 end
41 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
41 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
42 # Redirects user to the current version if he's not allowed to view previous versions
42 # Redirects user to the current version if he's not allowed to view previous versions
43 redirect_to :version => nil
43 redirect_to :version => nil
44 return
44 return
45 end
45 end
46 @content = @page.content_for_version(params[:version])
46 @content = @page.content_for_version(params[:version])
47 if params[:export] == 'html'
47 if params[:export] == 'html'
48 export = render_to_string :action => 'export', :layout => false
48 export = render_to_string :action => 'export', :layout => false
49 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
49 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
50 return
50 return
51 elsif params[:export] == 'txt'
51 elsif params[:export] == 'txt'
52 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
52 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
53 return
53 return
54 end
54 end
55 @editable = editable?
55 @editable = editable?
56 render :action => 'show'
56 render :action => 'show'
57 end
57 end
58
58
59 # edit an existing page or a new one
59 # edit an existing page or a new one
60 def edit
60 def edit
61 @page = @wiki.find_or_new_page(params[:page])
61 @page = @wiki.find_or_new_page(params[:page])
62 return render_403 unless editable?
62 return render_403 unless editable?
63 @page.content = WikiContent.new(:page => @page) if @page.new_record?
63 @page.content = WikiContent.new(:page => @page) if @page.new_record?
64
64
65 @content = @page.content_for_version(params[:version])
65 @content = @page.content_for_version(params[:version])
66 @content.text = initial_page_content(@page) if @content.text.blank?
66 @content.text = initial_page_content(@page) if @content.text.blank?
67 # don't keep previous comment
67 # don't keep previous comment
68 @content.comments = nil
68 @content.comments = nil
69 if request.get?
69 if request.get?
70 # To prevent StaleObjectError exception when reverting to a previous version
70 # To prevent StaleObjectError exception when reverting to a previous version
71 @content.version = @page.content.version
71 @content.version = @page.content.version
72 else
72 else
73 if !@page.new_record? && @content.text == params[:content][:text]
73 if !@page.new_record? && @content.text == params[:content][:text]
74 # don't save if text wasn't changed
74 # don't save if text wasn't changed
75 redirect_to :action => 'index', :id => @project, :page => @page.title
75 redirect_to :action => 'index', :id => @project, :page => @page.title
76 return
76 return
77 end
77 end
78 #@content.text = params[:content][:text]
78 #@content.text = params[:content][:text]
79 #@content.comments = params[:content][:comments]
79 #@content.comments = params[:content][:comments]
80 @content.attributes = params[:content]
80 @content.attributes = params[:content]
81 @content.author = User.current
81 @content.author = User.current
82 # if page is new @page.save will also save content, but not if page isn't a new record
82 # if page is new @page.save will also save content, but not if page isn't a new record
83 if (@page.new_record? ? @page.save : @content.save)
83 if (@page.new_record? ? @page.save : @content.save)
84 redirect_to :action => 'index', :id => @project, :page => @page.title
84 redirect_to :action => 'index', :id => @project, :page => @page.title
85 end
85 end
86 end
86 end
87 rescue ActiveRecord::StaleObjectError
87 rescue ActiveRecord::StaleObjectError
88 # Optimistic locking exception
88 # Optimistic locking exception
89 flash[:error] = l(:notice_locking_conflict)
89 flash[:error] = l(:notice_locking_conflict)
90 end
90 end
91
91
92 # rename a page
92 # rename a page
93 def rename
93 def rename
94 @page = @wiki.find_page(params[:page])
94 @page = @wiki.find_page(params[:page])
95 return render_403 unless editable?
95 return render_403 unless editable?
96 @page.redirect_existing_links = true
96 @page.redirect_existing_links = true
97 # used to display the *original* title if some AR validation errors occur
97 # used to display the *original* title if some AR validation errors occur
98 @original_title = @page.pretty_title
98 @original_title = @page.pretty_title
99 if request.post? && @page.update_attributes(params[:wiki_page])
99 if request.post? && @page.update_attributes(params[:wiki_page])
100 flash[:notice] = l(:notice_successful_update)
100 flash[:notice] = l(:notice_successful_update)
101 redirect_to :action => 'index', :id => @project, :page => @page.title
101 redirect_to :action => 'index', :id => @project, :page => @page.title
102 end
102 end
103 end
103 end
104
104
105 def protect
105 def protect
106 page = @wiki.find_page(params[:page])
106 page = @wiki.find_page(params[:page])
107 page.update_attribute :protected, params[:protected]
107 page.update_attribute :protected, params[:protected]
108 redirect_to :action => 'index', :id => @project, :page => page.title
108 redirect_to :action => 'index', :id => @project, :page => page.title
109 end
109 end
110
110
111 # show page history
111 # show page history
112 def history
112 def history
113 @page = @wiki.find_page(params[:page])
113 @page = @wiki.find_page(params[:page])
114
114
115 @version_count = @page.content.versions.count
115 @version_count = @page.content.versions.count
116 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
116 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
117 # don't load text
117 # don't load text
118 @versions = @page.content.versions.find :all,
118 @versions = @page.content.versions.find :all,
119 :select => "id, author_id, comments, updated_on, version",
119 :select => "id, author_id, comments, updated_on, version",
120 :order => 'version DESC',
120 :order => 'version DESC',
121 :limit => @version_pages.items_per_page + 1,
121 :limit => @version_pages.items_per_page + 1,
122 :offset => @version_pages.current.offset
122 :offset => @version_pages.current.offset
123
123
124 render :layout => false if request.xhr?
124 render :layout => false if request.xhr?
125 end
125 end
126
126
127 def diff
127 def diff
128 @page = @wiki.find_page(params[:page])
128 @page = @wiki.find_page(params[:page])
129 @diff = @page.diff(params[:version], params[:version_from])
129 @diff = @page.diff(params[:version], params[:version_from])
130 render_404 unless @diff
130 render_404 unless @diff
131 end
131 end
132
132
133 def annotate
133 def annotate
134 @page = @wiki.find_page(params[:page])
134 @page = @wiki.find_page(params[:page])
135 @annotate = @page.annotate(params[:version])
135 @annotate = @page.annotate(params[:version])
136 end
136 end
137
137
138 # remove a wiki page and its history
138 # remove a wiki page and its history
139 def destroy
139 def destroy
140 @page = @wiki.find_page(params[:page])
140 @page = @wiki.find_page(params[:page])
141 return render_403 unless editable?
141 return render_403 unless editable?
142 @page.destroy if @page
142 @page.destroy if @page
143 redirect_to :action => 'special', :id => @project, :page => 'Page_index'
143 redirect_to :action => 'special', :id => @project, :page => 'Page_index'
144 end
144 end
145
145
146 # display special pages
146 # display special pages
147 def special
147 def special
148 page_title = params[:page].downcase
148 page_title = params[:page].downcase
149 case page_title
149 case page_title
150 # show pages index, sorted by title
150 # show pages index, sorted by title
151 when 'page_index', 'date_index'
151 when 'page_index', 'date_index'
152 # eager load information about last updates, without loading text
152 # eager load information about last updates, without loading text
153 @pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
153 @pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
154 :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
154 :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
155 :order => 'title'
155 :order => 'title'
156 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
156 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
157 @pages_by_parent_id = @pages.group_by(&:parent_id)
157 @pages_by_parent_id = @pages.group_by(&:parent_id)
158 # export wiki to a single html file
158 # export wiki to a single html file
159 when 'export'
159 when 'export'
160 @pages = @wiki.pages.find :all, :order => 'title'
160 @pages = @wiki.pages.find :all, :order => 'title'
161 export = render_to_string :action => 'export_multiple', :layout => false
161 export = render_to_string :action => 'export_multiple', :layout => false
162 send_data(export, :type => 'text/html', :filename => "wiki.html")
162 send_data(export, :type => 'text/html', :filename => "wiki.html")
163 return
163 return
164 else
164 else
165 # requested special page doesn't exist, redirect to default page
165 # requested special page doesn't exist, redirect to default page
166 redirect_to :action => 'index', :id => @project, :page => nil and return
166 redirect_to :action => 'index', :id => @project, :page => nil and return
167 end
167 end
168 render :action => "special_#{page_title}"
168 render :action => "special_#{page_title}"
169 end
169 end
170
170
171 def preview
171 def preview
172 page = @wiki.find_page(params[:page])
172 page = @wiki.find_page(params[:page])
173 # page is nil when previewing a new page
173 # page is nil when previewing a new page
174 return render_403 unless page.nil? || editable?(page)
174 return render_403 unless page.nil? || editable?(page)
175 if page
175 if page
176 @attachements = page.attachments
176 @attachements = page.attachments
177 @previewed = page.content
177 @previewed = page.content
178 end
178 end
179 @text = params[:content][:text]
179 @text = params[:content][:text]
180 render :partial => 'common/preview'
180 render :partial => 'common/preview'
181 end
181 end
182
182
183 def add_attachment
183 def add_attachment
184 @page = @wiki.find_page(params[:page])
184 @page = @wiki.find_page(params[:page])
185 return render_403 unless editable?
185 return render_403 unless editable?
186 attach_files(@page, params[:attachments])
186 attach_files(@page, params[:attachments])
187 redirect_to :action => 'index', :page => @page.title
187 redirect_to :action => 'index', :page => @page.title
188 end
188 end
189
189
190 def destroy_attachment
191 @page = @wiki.find_page(params[:page])
192 return render_403 unless editable?
193 @page.attachments.find(params[:attachment_id]).destroy
194 redirect_to :action => 'index', :page => @page.title
195 end
196
197 private
190 private
198
191
199 def find_wiki
192 def find_wiki
200 @project = Project.find(params[:id])
193 @project = Project.find(params[:id])
201 @wiki = @project.wiki
194 @wiki = @project.wiki
202 render_404 unless @wiki
195 render_404 unless @wiki
203 rescue ActiveRecord::RecordNotFound
196 rescue ActiveRecord::RecordNotFound
204 render_404
197 render_404
205 end
198 end
206
199
207 # Returns true if the current user is allowed to edit the page, otherwise false
200 # Returns true if the current user is allowed to edit the page, otherwise false
208 def editable?(page = @page)
201 def editable?(page = @page)
209 page.editable_by?(User.current)
202 page.editable_by?(User.current)
210 end
203 end
211
204
212 # Returns the default content of a new wiki page
205 # Returns the default content of a new wiki page
213 def initial_page_content(page)
206 def initial_page_content(page)
214 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
207 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
215 extend helper unless self.instance_of?(helper)
208 extend helper unless self.instance_of?(helper)
216 helper.instance_method(:initial_page_content).bind(self).call(page)
209 helper.instance_method(:initial_page_content).bind(self).call(page)
217 end
210 end
218 end
211 end
@@ -1,29 +1,34
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 module AttachmentsHelper
18 module AttachmentsHelper
19 # displays the links to a collection of attachments
19 # Displays view/delete links to the attachments of the given object
20 def link_to_attachments(attachments, options = {})
20 # Options:
21 if attachments.any?
21 # :author -- author names are not displayed if set to false
22 render :partial => 'attachments/links', :locals => {:attachments => attachments, :options => options}
22 def link_to_attachments(container, options = {})
23 options.assert_valid_keys(:author)
24
25 if container.attachments.any?
26 options = {:deletable => container.attachments_deletable?, :author => true}.merge(options)
27 render :partial => 'attachments/links', :locals => {:attachments => container.attachments, :options => options}
23 end
28 end
24 end
29 end
25
30
26 def to_utf8(str)
31 def to_utf8(str)
27 str
32 str
28 end
33 end
29 end
34 end
@@ -1,143 +1,151
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 "digest/md5"
18 require "digest/md5"
19
19
20 class Attachment < ActiveRecord::Base
20 class Attachment < ActiveRecord::Base
21 belongs_to :container, :polymorphic => true
21 belongs_to :container, :polymorphic => true
22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
23
23
24 validates_presence_of :container, :filename, :author
24 validates_presence_of :container, :filename, :author
25 validates_length_of :filename, :maximum => 255
25 validates_length_of :filename, :maximum => 255
26 validates_length_of :disk_filename, :maximum => 255
26 validates_length_of :disk_filename, :maximum => 255
27
27
28 acts_as_event :title => :filename,
28 acts_as_event :title => :filename,
29 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
29 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
30
30
31 acts_as_activity_provider :type => 'files',
31 acts_as_activity_provider :type => 'files',
32 :permission => :view_files,
32 :permission => :view_files,
33 :author_key => :author_id,
33 :author_key => :author_id,
34 :find_options => {:select => "#{Attachment.table_name}.*",
34 :find_options => {:select => "#{Attachment.table_name}.*",
35 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
35 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
36 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id"}
36 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id"}
37
37
38 acts_as_activity_provider :type => 'documents',
38 acts_as_activity_provider :type => 'documents',
39 :permission => :view_documents,
39 :permission => :view_documents,
40 :author_key => :author_id,
40 :author_key => :author_id,
41 :find_options => {:select => "#{Attachment.table_name}.*",
41 :find_options => {:select => "#{Attachment.table_name}.*",
42 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
42 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
43 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
43 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
44
44
45 cattr_accessor :storage_path
45 cattr_accessor :storage_path
46 @@storage_path = "#{RAILS_ROOT}/files"
46 @@storage_path = "#{RAILS_ROOT}/files"
47
47
48 def validate
48 def validate
49 errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes
49 errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes
50 end
50 end
51
51
52 def file=(incoming_file)
52 def file=(incoming_file)
53 unless incoming_file.nil?
53 unless incoming_file.nil?
54 @temp_file = incoming_file
54 @temp_file = incoming_file
55 if @temp_file.size > 0
55 if @temp_file.size > 0
56 self.filename = sanitize_filename(@temp_file.original_filename)
56 self.filename = sanitize_filename(@temp_file.original_filename)
57 self.disk_filename = Attachment.disk_filename(filename)
57 self.disk_filename = Attachment.disk_filename(filename)
58 self.content_type = @temp_file.content_type.to_s.chomp
58 self.content_type = @temp_file.content_type.to_s.chomp
59 self.filesize = @temp_file.size
59 self.filesize = @temp_file.size
60 end
60 end
61 end
61 end
62 end
62 end
63
63
64 def file
64 def file
65 nil
65 nil
66 end
66 end
67
67
68 # Copy temp file to its final location
68 # Copy temp file to its final location
69 def before_save
69 def before_save
70 if @temp_file && (@temp_file.size > 0)
70 if @temp_file && (@temp_file.size > 0)
71 logger.debug("saving '#{self.diskfile}'")
71 logger.debug("saving '#{self.diskfile}'")
72 File.open(diskfile, "wb") do |f|
72 File.open(diskfile, "wb") do |f|
73 f.write(@temp_file.read)
73 f.write(@temp_file.read)
74 end
74 end
75 self.digest = self.class.digest(diskfile)
75 self.digest = self.class.digest(diskfile)
76 end
76 end
77 # Don't save the content type if it's longer than the authorized length
77 # Don't save the content type if it's longer than the authorized length
78 if self.content_type && self.content_type.length > 255
78 if self.content_type && self.content_type.length > 255
79 self.content_type = nil
79 self.content_type = nil
80 end
80 end
81 end
81 end
82
82
83 # Deletes file on the disk
83 # Deletes file on the disk
84 def after_destroy
84 def after_destroy
85 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
85 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
86 end
86 end
87
87
88 # Returns file's location on disk
88 # Returns file's location on disk
89 def diskfile
89 def diskfile
90 "#{@@storage_path}/#{self.disk_filename}"
90 "#{@@storage_path}/#{self.disk_filename}"
91 end
91 end
92
92
93 def increment_download
93 def increment_download
94 increment!(:downloads)
94 increment!(:downloads)
95 end
95 end
96
96
97 def project
97 def project
98 container.project
98 container.project
99 end
99 end
100
100
101 def visible?(user=User.current)
102 container.attachments_visible?(user)
103 end
104
105 def deletable?(user=User.current)
106 container.attachments_deletable?(user)
107 end
108
101 def image?
109 def image?
102 self.filename =~ /\.(jpe?g|gif|png)$/i
110 self.filename =~ /\.(jpe?g|gif|png)$/i
103 end
111 end
104
112
105 def is_text?
113 def is_text?
106 Redmine::MimeType.is_type?('text', filename)
114 Redmine::MimeType.is_type?('text', filename)
107 end
115 end
108
116
109 def is_diff?
117 def is_diff?
110 self.filename =~ /\.(patch|diff)$/i
118 self.filename =~ /\.(patch|diff)$/i
111 end
119 end
112
120
113 private
121 private
114 def sanitize_filename(value)
122 def sanitize_filename(value)
115 # get only the filename, not the whole path
123 # get only the filename, not the whole path
116 just_filename = value.gsub(/^.*(\\|\/)/, '')
124 just_filename = value.gsub(/^.*(\\|\/)/, '')
117 # NOTE: File.basename doesn't work right with Windows paths on Unix
125 # NOTE: File.basename doesn't work right with Windows paths on Unix
118 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
126 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
119
127
120 # Finally, replace all non alphanumeric, hyphens or periods with underscore
128 # Finally, replace all non alphanumeric, hyphens or periods with underscore
121 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
129 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
122 end
130 end
123
131
124 # Returns an ASCII or hashed filename
132 # Returns an ASCII or hashed filename
125 def self.disk_filename(filename)
133 def self.disk_filename(filename)
126 df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
134 df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
127 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
135 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
128 df << filename
136 df << filename
129 else
137 else
130 df << Digest::MD5.hexdigest(filename)
138 df << Digest::MD5.hexdigest(filename)
131 # keep the extension if any
139 # keep the extension if any
132 df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
140 df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
133 end
141 end
134 df
142 df
135 end
143 end
136
144
137 # Returns the MD5 digest of the file at given path
145 # Returns the MD5 digest of the file at given path
138 def self.digest(filename)
146 def self.digest(filename)
139 File.open(filename, 'rb') do |f|
147 File.open(filename, 'rb') do |f|
140 Digest::MD5.hexdigest(f.read)
148 Digest::MD5.hexdigest(f.read)
141 end
149 end
142 end
150 end
143 end
151 end
@@ -1,31 +1,31
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 Document < ActiveRecord::Base
18 class Document < ActiveRecord::Base
19 belongs_to :project
19 belongs_to :project
20 belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
20 belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
21 has_many :attachments, :as => :container, :dependent => :destroy
21 acts_as_attachable :delete_permission => :manage_documents
22
22
23 acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
23 acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
24 acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
24 acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
25 :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
25 :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
26 :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
26 :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
27 acts_as_activity_provider :find_options => {:include => :project}
27 acts_as_activity_provider :find_options => {:include => :project}
28
28
29 validates_presence_of :project, :title, :category
29 validates_presence_of :project, :title, :category
30 validates_length_of :title, :maximum => 60
30 validates_length_of :title, :maximum => 60
31 end
31 end
@@ -1,264 +1,275
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 belongs_to :project
19 belongs_to :project
20 belongs_to :tracker
20 belongs_to :tracker
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
25 belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27
27
28 has_many :journals, :as => :journalized, :dependent => :destroy
28 has_many :journals, :as => :journalized, :dependent => :destroy
29 has_many :attachments, :as => :container, :dependent => :destroy
30 has_many :time_entries, :dependent => :delete_all
29 has_many :time_entries, :dependent => :delete_all
31 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
32
31
33 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
34 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
35
34
35 acts_as_attachable :after_remove => :attachment_removed
36 acts_as_customizable
36 acts_as_customizable
37 acts_as_watchable
37 acts_as_watchable
38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 :include => [:project, :journals],
39 :include => [:project, :journals],
40 # sort by id so that limited eager loading doesn't break with postgresql
40 # sort by id so that limited eager loading doesn't break with postgresql
41 :order_column => "#{table_name}.id"
41 :order_column => "#{table_name}.id"
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
44
44
45 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
45 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
46 :author_key => :author_id
46 :author_key => :author_id
47
47
48 validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
48 validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
49 validates_length_of :subject, :maximum => 255
49 validates_length_of :subject, :maximum => 255
50 validates_inclusion_of :done_ratio, :in => 0..100
50 validates_inclusion_of :done_ratio, :in => 0..100
51 validates_numericality_of :estimated_hours, :allow_nil => true
51 validates_numericality_of :estimated_hours, :allow_nil => true
52
52
53 def after_initialize
53 def after_initialize
54 if new_record?
54 if new_record?
55 # set default values for new records only
55 # set default values for new records only
56 self.status ||= IssueStatus.default
56 self.status ||= IssueStatus.default
57 self.priority ||= Enumeration.default('IPRI')
57 self.priority ||= Enumeration.default('IPRI')
58 end
58 end
59 end
59 end
60
60
61 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
61 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
62 def available_custom_fields
62 def available_custom_fields
63 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
63 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
64 end
64 end
65
65
66 def copy_from(arg)
66 def copy_from(arg)
67 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
67 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
68 self.attributes = issue.attributes.dup
68 self.attributes = issue.attributes.dup
69 self.custom_values = issue.custom_values.collect {|v| v.clone}
69 self.custom_values = issue.custom_values.collect {|v| v.clone}
70 self
70 self
71 end
71 end
72
72
73 # Move an issue to a new project and tracker
73 # Move an issue to a new project and tracker
74 def move_to(new_project, new_tracker = nil)
74 def move_to(new_project, new_tracker = nil)
75 transaction do
75 transaction do
76 if new_project && project_id != new_project.id
76 if new_project && project_id != new_project.id
77 # delete issue relations
77 # delete issue relations
78 unless Setting.cross_project_issue_relations?
78 unless Setting.cross_project_issue_relations?
79 self.relations_from.clear
79 self.relations_from.clear
80 self.relations_to.clear
80 self.relations_to.clear
81 end
81 end
82 # issue is moved to another project
82 # issue is moved to another project
83 # reassign to the category with same name if any
83 # reassign to the category with same name if any
84 new_category = category.nil? ? nil : new_project.issue_categories.find_by_name(category.name)
84 new_category = category.nil? ? nil : new_project.issue_categories.find_by_name(category.name)
85 self.category = new_category
85 self.category = new_category
86 self.fixed_version = nil
86 self.fixed_version = nil
87 self.project = new_project
87 self.project = new_project
88 end
88 end
89 if new_tracker
89 if new_tracker
90 self.tracker = new_tracker
90 self.tracker = new_tracker
91 end
91 end
92 if save
92 if save
93 # Manually update project_id on related time entries
93 # Manually update project_id on related time entries
94 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
94 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
95 else
95 else
96 rollback_db_transaction
96 rollback_db_transaction
97 return false
97 return false
98 end
98 end
99 end
99 end
100 return true
100 return true
101 end
101 end
102
102
103 def priority_id=(pid)
103 def priority_id=(pid)
104 self.priority = nil
104 self.priority = nil
105 write_attribute(:priority_id, pid)
105 write_attribute(:priority_id, pid)
106 end
106 end
107
107
108 def estimated_hours=(h)
108 def estimated_hours=(h)
109 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
109 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
110 end
110 end
111
111
112 def validate
112 def validate
113 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
113 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
114 errors.add :due_date, :activerecord_error_not_a_date
114 errors.add :due_date, :activerecord_error_not_a_date
115 end
115 end
116
116
117 if self.due_date and self.start_date and self.due_date < self.start_date
117 if self.due_date and self.start_date and self.due_date < self.start_date
118 errors.add :due_date, :activerecord_error_greater_than_start_date
118 errors.add :due_date, :activerecord_error_greater_than_start_date
119 end
119 end
120
120
121 if start_date && soonest_start && start_date < soonest_start
121 if start_date && soonest_start && start_date < soonest_start
122 errors.add :start_date, :activerecord_error_invalid
122 errors.add :start_date, :activerecord_error_invalid
123 end
123 end
124 end
124 end
125
125
126 def validate_on_create
126 def validate_on_create
127 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
127 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
128 end
128 end
129
129
130 def before_create
130 def before_create
131 # default assignment based on category
131 # default assignment based on category
132 if assigned_to.nil? && category && category.assigned_to
132 if assigned_to.nil? && category && category.assigned_to
133 self.assigned_to = category.assigned_to
133 self.assigned_to = category.assigned_to
134 end
134 end
135 end
135 end
136
136
137 def before_save
137 def before_save
138 if @current_journal
138 if @current_journal
139 # attributes changes
139 # attributes changes
140 (Issue.column_names - %w(id description)).each {|c|
140 (Issue.column_names - %w(id description)).each {|c|
141 @current_journal.details << JournalDetail.new(:property => 'attr',
141 @current_journal.details << JournalDetail.new(:property => 'attr',
142 :prop_key => c,
142 :prop_key => c,
143 :old_value => @issue_before_change.send(c),
143 :old_value => @issue_before_change.send(c),
144 :value => send(c)) unless send(c)==@issue_before_change.send(c)
144 :value => send(c)) unless send(c)==@issue_before_change.send(c)
145 }
145 }
146 # custom fields changes
146 # custom fields changes
147 custom_values.each {|c|
147 custom_values.each {|c|
148 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
148 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
149 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
149 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
150 @current_journal.details << JournalDetail.new(:property => 'cf',
150 @current_journal.details << JournalDetail.new(:property => 'cf',
151 :prop_key => c.custom_field_id,
151 :prop_key => c.custom_field_id,
152 :old_value => @custom_values_before_change[c.custom_field_id],
152 :old_value => @custom_values_before_change[c.custom_field_id],
153 :value => c.value)
153 :value => c.value)
154 }
154 }
155 @current_journal.save
155 @current_journal.save
156 end
156 end
157 # Save the issue even if the journal is not saved (because empty)
157 # Save the issue even if the journal is not saved (because empty)
158 true
158 true
159 end
159 end
160
160
161 def after_save
161 def after_save
162 # Reload is needed in order to get the right status
162 # Reload is needed in order to get the right status
163 reload
163 reload
164
164
165 # Update start/due dates of following issues
165 # Update start/due dates of following issues
166 relations_from.each(&:set_issue_to_dates)
166 relations_from.each(&:set_issue_to_dates)
167
167
168 # Close duplicates if the issue was closed
168 # Close duplicates if the issue was closed
169 if @issue_before_change && !@issue_before_change.closed? && self.closed?
169 if @issue_before_change && !@issue_before_change.closed? && self.closed?
170 duplicates.each do |duplicate|
170 duplicates.each do |duplicate|
171 # Reload is need in case the duplicate was updated by a previous duplicate
171 # Reload is need in case the duplicate was updated by a previous duplicate
172 duplicate.reload
172 duplicate.reload
173 # Don't re-close it if it's already closed
173 # Don't re-close it if it's already closed
174 next if duplicate.closed?
174 next if duplicate.closed?
175 # Same user and notes
175 # Same user and notes
176 duplicate.init_journal(@current_journal.user, @current_journal.notes)
176 duplicate.init_journal(@current_journal.user, @current_journal.notes)
177 duplicate.update_attribute :status, self.status
177 duplicate.update_attribute :status, self.status
178 end
178 end
179 end
179 end
180 end
180 end
181
181
182 def init_journal(user, notes = "")
182 def init_journal(user, notes = "")
183 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
183 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
184 @issue_before_change = self.clone
184 @issue_before_change = self.clone
185 @issue_before_change.status = self.status
185 @issue_before_change.status = self.status
186 @custom_values_before_change = {}
186 @custom_values_before_change = {}
187 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
187 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
188 # Make sure updated_on is updated when adding a note.
188 # Make sure updated_on is updated when adding a note.
189 updated_on_will_change!
189 updated_on_will_change!
190 @current_journal
190 @current_journal
191 end
191 end
192
192
193 # Return true if the issue is closed, otherwise false
193 # Return true if the issue is closed, otherwise false
194 def closed?
194 def closed?
195 self.status.is_closed?
195 self.status.is_closed?
196 end
196 end
197
197
198 # Users the issue can be assigned to
198 # Users the issue can be assigned to
199 def assignable_users
199 def assignable_users
200 project.assignable_users
200 project.assignable_users
201 end
201 end
202
202
203 # Returns an array of status that user is able to apply
203 # Returns an array of status that user is able to apply
204 def new_statuses_allowed_to(user)
204 def new_statuses_allowed_to(user)
205 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
205 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
206 statuses << status unless statuses.empty?
206 statuses << status unless statuses.empty?
207 statuses.uniq.sort
207 statuses.uniq.sort
208 end
208 end
209
209
210 # Returns the mail adresses of users that should be notified for the issue
210 # Returns the mail adresses of users that should be notified for the issue
211 def recipients
211 def recipients
212 recipients = project.recipients
212 recipients = project.recipients
213 # Author and assignee are always notified unless they have been locked
213 # Author and assignee are always notified unless they have been locked
214 recipients << author.mail if author && author.active?
214 recipients << author.mail if author && author.active?
215 recipients << assigned_to.mail if assigned_to && assigned_to.active?
215 recipients << assigned_to.mail if assigned_to && assigned_to.active?
216 recipients.compact.uniq
216 recipients.compact.uniq
217 end
217 end
218
218
219 def spent_hours
219 def spent_hours
220 @spent_hours ||= time_entries.sum(:hours) || 0
220 @spent_hours ||= time_entries.sum(:hours) || 0
221 end
221 end
222
222
223 def relations
223 def relations
224 (relations_from + relations_to).sort
224 (relations_from + relations_to).sort
225 end
225 end
226
226
227 def all_dependent_issues
227 def all_dependent_issues
228 dependencies = []
228 dependencies = []
229 relations_from.each do |relation|
229 relations_from.each do |relation|
230 dependencies << relation.issue_to
230 dependencies << relation.issue_to
231 dependencies += relation.issue_to.all_dependent_issues
231 dependencies += relation.issue_to.all_dependent_issues
232 end
232 end
233 dependencies
233 dependencies
234 end
234 end
235
235
236 # Returns an array of issues that duplicate this one
236 # Returns an array of issues that duplicate this one
237 def duplicates
237 def duplicates
238 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
238 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
239 end
239 end
240
240
241 # Returns the due date or the target due date if any
241 # Returns the due date or the target due date if any
242 # Used on gantt chart
242 # Used on gantt chart
243 def due_before
243 def due_before
244 due_date || (fixed_version ? fixed_version.effective_date : nil)
244 due_date || (fixed_version ? fixed_version.effective_date : nil)
245 end
245 end
246
246
247 def duration
247 def duration
248 (start_date && due_date) ? due_date - start_date : 0
248 (start_date && due_date) ? due_date - start_date : 0
249 end
249 end
250
250
251 def soonest_start
251 def soonest_start
252 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
252 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
253 end
253 end
254
254
255 def self.visible_by(usr)
255 def self.visible_by(usr)
256 with_scope(:find => { :conditions => Project.visible_by(usr) }) do
256 with_scope(:find => { :conditions => Project.visible_by(usr) }) do
257 yield
257 yield
258 end
258 end
259 end
259 end
260
260
261 def to_s
261 def to_s
262 "#{tracker} ##{id}: #{subject}"
262 "#{tracker} ##{id}: #{subject}"
263 end
263 end
264
265 private
266
267 # Callback on attachment deletion
268 def attachment_removed(obj)
269 journal = init_journal(User.current)
270 journal.details << JournalDetail.new(:property => 'attachment',
271 :prop_key => obj.id,
272 :old_value => obj.filename)
273 journal.save
274 end
264 end
275 end
@@ -1,89 +1,89
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 Message < ActiveRecord::Base
18 class Message < ActiveRecord::Base
19 belongs_to :board
19 belongs_to :board
20 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
20 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
21 acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
21 acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
22 has_many :attachments, :as => :container, :dependent => :destroy
22 acts_as_attachable
23 belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
23 belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
24
24
25 acts_as_searchable :columns => ['subject', 'content'],
25 acts_as_searchable :columns => ['subject', 'content'],
26 :include => {:board, :project},
26 :include => {:board, :project},
27 :project_key => 'project_id',
27 :project_key => 'project_id',
28 :date_column => "#{table_name}.created_on"
28 :date_column => "#{table_name}.created_on"
29 acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
29 acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
30 :description => :content,
30 :description => :content,
31 :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
31 :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
32 :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
32 :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
33 {:id => o.parent_id, :anchor => "message-#{o.id}"})}
33 {:id => o.parent_id, :anchor => "message-#{o.id}"})}
34
34
35 acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]},
35 acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]},
36 :author_key => :author_id
36 :author_key => :author_id
37 acts_as_watchable
37 acts_as_watchable
38
38
39 attr_protected :locked, :sticky
39 attr_protected :locked, :sticky
40 validates_presence_of :subject, :content
40 validates_presence_of :subject, :content
41 validates_length_of :subject, :maximum => 255
41 validates_length_of :subject, :maximum => 255
42
42
43 after_create :add_author_as_watcher
43 after_create :add_author_as_watcher
44
44
45 def validate_on_create
45 def validate_on_create
46 # Can not reply to a locked topic
46 # Can not reply to a locked topic
47 errors.add_to_base 'Topic is locked' if root.locked? && self != root
47 errors.add_to_base 'Topic is locked' if root.locked? && self != root
48 end
48 end
49
49
50 def after_create
50 def after_create
51 board.update_attribute(:last_message_id, self.id)
51 board.update_attribute(:last_message_id, self.id)
52 board.increment! :messages_count
52 board.increment! :messages_count
53 if parent
53 if parent
54 parent.reload.update_attribute(:last_reply_id, self.id)
54 parent.reload.update_attribute(:last_reply_id, self.id)
55 else
55 else
56 board.increment! :topics_count
56 board.increment! :topics_count
57 end
57 end
58 end
58 end
59
59
60 def after_destroy
60 def after_destroy
61 # The following line is required so that the previous counter
61 # The following line is required so that the previous counter
62 # updates (due to children removal) are not overwritten
62 # updates (due to children removal) are not overwritten
63 board.reload
63 board.reload
64 board.decrement! :messages_count
64 board.decrement! :messages_count
65 board.decrement! :topics_count unless parent
65 board.decrement! :topics_count unless parent
66 end
66 end
67
67
68 def sticky?
68 def sticky?
69 sticky == 1
69 sticky == 1
70 end
70 end
71
71
72 def project
72 def project
73 board.project
73 board.project
74 end
74 end
75
75
76 def editable_by?(usr)
76 def editable_by?(usr)
77 usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)))
77 usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)))
78 end
78 end
79
79
80 def destroyable_by?(usr)
80 def destroyable_by?(usr)
81 usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
81 usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
82 end
82 end
83
83
84 private
84 private
85
85
86 def add_author_as_watcher
86 def add_author_as_watcher
87 Watcher.create(:watchable => self.root, :user => author)
87 Watcher.create(:watchable => self.root, :user => author)
88 end
88 end
89 end
89 end
@@ -1,106 +1,107
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 Version < ActiveRecord::Base
18 class Version < ActiveRecord::Base
19 before_destroy :check_integrity
19 before_destroy :check_integrity
20 belongs_to :project
20 belongs_to :project
21 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id'
21 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id'
22 has_many :attachments, :as => :container, :dependent => :destroy
22 acts_as_attachable :view_permission => :view_files,
23 :delete_permission => :manage_files
23
24
24 validates_presence_of :name
25 validates_presence_of :name
25 validates_uniqueness_of :name, :scope => [:project_id]
26 validates_uniqueness_of :name, :scope => [:project_id]
26 validates_length_of :name, :maximum => 60
27 validates_length_of :name, :maximum => 60
27 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => 'activerecord_error_not_a_date', :allow_nil => true
28 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => 'activerecord_error_not_a_date', :allow_nil => true
28
29
29 def start_date
30 def start_date
30 effective_date
31 effective_date
31 end
32 end
32
33
33 def due_date
34 def due_date
34 effective_date
35 effective_date
35 end
36 end
36
37
37 # Returns the total estimated time for this version
38 # Returns the total estimated time for this version
38 def estimated_hours
39 def estimated_hours
39 @estimated_hours ||= fixed_issues.sum(:estimated_hours).to_f
40 @estimated_hours ||= fixed_issues.sum(:estimated_hours).to_f
40 end
41 end
41
42
42 # Returns the total reported time for this version
43 # Returns the total reported time for this version
43 def spent_hours
44 def spent_hours
44 @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
45 @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
45 end
46 end
46
47
47 # Returns true if the version is completed: due date reached and no open issues
48 # Returns true if the version is completed: due date reached and no open issues
48 def completed?
49 def completed?
49 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
50 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
50 end
51 end
51
52
52 def completed_pourcent
53 def completed_pourcent
53 if fixed_issues.count == 0
54 if fixed_issues.count == 0
54 0
55 0
55 elsif open_issues_count == 0
56 elsif open_issues_count == 0
56 100
57 100
57 else
58 else
58 (closed_issues_count * 100 + Issue.sum('done_ratio', :include => 'status', :conditions => ["fixed_version_id = ? AND is_closed = ?", id, false]).to_f) / fixed_issues.count
59 (closed_issues_count * 100 + Issue.sum('done_ratio', :include => 'status', :conditions => ["fixed_version_id = ? AND is_closed = ?", id, false]).to_f) / fixed_issues.count
59 end
60 end
60 end
61 end
61
62
62 def closed_pourcent
63 def closed_pourcent
63 if fixed_issues.count == 0
64 if fixed_issues.count == 0
64 0
65 0
65 else
66 else
66 closed_issues_count * 100.0 / fixed_issues.count
67 closed_issues_count * 100.0 / fixed_issues.count
67 end
68 end
68 end
69 end
69
70
70 # Returns true if the version is overdue: due date reached and some open issues
71 # Returns true if the version is overdue: due date reached and some open issues
71 def overdue?
72 def overdue?
72 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
73 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
73 end
74 end
74
75
75 def open_issues_count
76 def open_issues_count
76 @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
77 @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
77 end
78 end
78
79
79 def closed_issues_count
80 def closed_issues_count
80 @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
81 @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
81 end
82 end
82
83
83 def wiki_page
84 def wiki_page
84 if project.wiki && !wiki_page_title.blank?
85 if project.wiki && !wiki_page_title.blank?
85 @wiki_page ||= project.wiki.find_page(wiki_page_title)
86 @wiki_page ||= project.wiki.find_page(wiki_page_title)
86 end
87 end
87 @wiki_page
88 @wiki_page
88 end
89 end
89
90
90 def to_s; name end
91 def to_s; name end
91
92
92 # Versions are sorted by effective_date and name
93 # Versions are sorted by effective_date and name
93 # Those with no effective_date are at the end, sorted by name
94 # Those with no effective_date are at the end, sorted by name
94 def <=>(version)
95 def <=>(version)
95 if self.effective_date
96 if self.effective_date
96 version.effective_date ? (self.effective_date == version.effective_date ? self.name <=> version.name : self.effective_date <=> version.effective_date) : -1
97 version.effective_date ? (self.effective_date == version.effective_date ? self.name <=> version.name : self.effective_date <=> version.effective_date) : -1
97 else
98 else
98 version.effective_date ? 1 : (self.name <=> version.name)
99 version.effective_date ? 1 : (self.name <=> version.name)
99 end
100 end
100 end
101 end
101
102
102 private
103 private
103 def check_integrity
104 def check_integrity
104 raise "Can't delete version" if self.fixed_issues.find(:first)
105 raise "Can't delete version" if self.fixed_issues.find(:first)
105 end
106 end
106 end
107 end
@@ -1,184 +1,188
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 'diff'
18 require 'diff'
19 require 'enumerator'
19 require 'enumerator'
20
20
21 class WikiPage < ActiveRecord::Base
21 class WikiPage < ActiveRecord::Base
22 belongs_to :wiki
22 belongs_to :wiki
23 has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
23 has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
24 has_many :attachments, :as => :container, :dependent => :destroy
24 acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
25 acts_as_tree :order => 'title'
25 acts_as_tree :order => 'title'
26
26
27 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
27 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
28 :description => :text,
28 :description => :text,
29 :datetime => :created_on,
29 :datetime => :created_on,
30 :url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}}
30 :url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}}
31
31
32 acts_as_searchable :columns => ['title', 'text'],
32 acts_as_searchable :columns => ['title', 'text'],
33 :include => [{:wiki => :project}, :content],
33 :include => [{:wiki => :project}, :content],
34 :project_key => "#{Wiki.table_name}.project_id"
34 :project_key => "#{Wiki.table_name}.project_id"
35
35
36 attr_accessor :redirect_existing_links
36 attr_accessor :redirect_existing_links
37
37
38 validates_presence_of :title
38 validates_presence_of :title
39 validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
39 validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
40 validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
40 validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
41 validates_associated :content
41 validates_associated :content
42
42
43 def title=(value)
43 def title=(value)
44 value = Wiki.titleize(value)
44 value = Wiki.titleize(value)
45 @previous_title = read_attribute(:title) if @previous_title.blank?
45 @previous_title = read_attribute(:title) if @previous_title.blank?
46 write_attribute(:title, value)
46 write_attribute(:title, value)
47 end
47 end
48
48
49 def before_save
49 def before_save
50 self.title = Wiki.titleize(title)
50 self.title = Wiki.titleize(title)
51 # Manage redirects if the title has changed
51 # Manage redirects if the title has changed
52 if !@previous_title.blank? && (@previous_title != title) && !new_record?
52 if !@previous_title.blank? && (@previous_title != title) && !new_record?
53 # Update redirects that point to the old title
53 # Update redirects that point to the old title
54 wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
54 wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
55 r.redirects_to = title
55 r.redirects_to = title
56 r.title == r.redirects_to ? r.destroy : r.save
56 r.title == r.redirects_to ? r.destroy : r.save
57 end
57 end
58 # Remove redirects for the new title
58 # Remove redirects for the new title
59 wiki.redirects.find_all_by_title(title).each(&:destroy)
59 wiki.redirects.find_all_by_title(title).each(&:destroy)
60 # Create a redirect to the new title
60 # Create a redirect to the new title
61 wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
61 wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
62 @previous_title = nil
62 @previous_title = nil
63 end
63 end
64 end
64 end
65
65
66 def before_destroy
66 def before_destroy
67 # Remove redirects to this page
67 # Remove redirects to this page
68 wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
68 wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
69 end
69 end
70
70
71 def pretty_title
71 def pretty_title
72 WikiPage.pretty_title(title)
72 WikiPage.pretty_title(title)
73 end
73 end
74
74
75 def content_for_version(version=nil)
75 def content_for_version(version=nil)
76 result = content.versions.find_by_version(version.to_i) if version
76 result = content.versions.find_by_version(version.to_i) if version
77 result ||= content
77 result ||= content
78 result
78 result
79 end
79 end
80
80
81 def diff(version_to=nil, version_from=nil)
81 def diff(version_to=nil, version_from=nil)
82 version_to = version_to ? version_to.to_i : self.content.version
82 version_to = version_to ? version_to.to_i : self.content.version
83 version_from = version_from ? version_from.to_i : version_to - 1
83 version_from = version_from ? version_from.to_i : version_to - 1
84 version_to, version_from = version_from, version_to unless version_from < version_to
84 version_to, version_from = version_from, version_to unless version_from < version_to
85
85
86 content_to = content.versions.find_by_version(version_to)
86 content_to = content.versions.find_by_version(version_to)
87 content_from = content.versions.find_by_version(version_from)
87 content_from = content.versions.find_by_version(version_from)
88
88
89 (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
89 (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
90 end
90 end
91
91
92 def annotate(version=nil)
92 def annotate(version=nil)
93 version = version ? version.to_i : self.content.version
93 version = version ? version.to_i : self.content.version
94 c = content.versions.find_by_version(version)
94 c = content.versions.find_by_version(version)
95 c ? WikiAnnotate.new(c) : nil
95 c ? WikiAnnotate.new(c) : nil
96 end
96 end
97
97
98 def self.pretty_title(str)
98 def self.pretty_title(str)
99 (str && str.is_a?(String)) ? str.tr('_', ' ') : str
99 (str && str.is_a?(String)) ? str.tr('_', ' ') : str
100 end
100 end
101
101
102 def project
102 def project
103 wiki.project
103 wiki.project
104 end
104 end
105
105
106 def text
106 def text
107 content.text if content
107 content.text if content
108 end
108 end
109
109
110 # Returns true if usr is allowed to edit the page, otherwise false
110 # Returns true if usr is allowed to edit the page, otherwise false
111 def editable_by?(usr)
111 def editable_by?(usr)
112 !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
112 !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
113 end
113 end
114
115 def attachments_deletable?(usr=User.current)
116 editable_by?(usr) && super(usr)
117 end
114
118
115 def parent_title
119 def parent_title
116 @parent_title || (self.parent && self.parent.pretty_title)
120 @parent_title || (self.parent && self.parent.pretty_title)
117 end
121 end
118
122
119 def parent_title=(t)
123 def parent_title=(t)
120 @parent_title = t
124 @parent_title = t
121 parent_page = t.blank? ? nil : self.wiki.find_page(t)
125 parent_page = t.blank? ? nil : self.wiki.find_page(t)
122 self.parent = parent_page
126 self.parent = parent_page
123 end
127 end
124
128
125 protected
129 protected
126
130
127 def validate
131 def validate
128 errors.add(:parent_title, :activerecord_error_invalid) if !@parent_title.blank? && parent.nil?
132 errors.add(:parent_title, :activerecord_error_invalid) if !@parent_title.blank? && parent.nil?
129 errors.add(:parent_title, :activerecord_error_circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
133 errors.add(:parent_title, :activerecord_error_circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
130 errors.add(:parent_title, :activerecord_error_not_same_project) if parent && (parent.wiki_id != wiki_id)
134 errors.add(:parent_title, :activerecord_error_not_same_project) if parent && (parent.wiki_id != wiki_id)
131 end
135 end
132 end
136 end
133
137
134 class WikiDiff
138 class WikiDiff
135 attr_reader :diff, :words, :content_to, :content_from
139 attr_reader :diff, :words, :content_to, :content_from
136
140
137 def initialize(content_to, content_from)
141 def initialize(content_to, content_from)
138 @content_to = content_to
142 @content_to = content_to
139 @content_from = content_from
143 @content_from = content_from
140 @words = content_to.text.split(/(\s+)/)
144 @words = content_to.text.split(/(\s+)/)
141 @words = @words.select {|word| word != ' '}
145 @words = @words.select {|word| word != ' '}
142 words_from = content_from.text.split(/(\s+)/)
146 words_from = content_from.text.split(/(\s+)/)
143 words_from = words_from.select {|word| word != ' '}
147 words_from = words_from.select {|word| word != ' '}
144 @diff = words_from.diff @words
148 @diff = words_from.diff @words
145 end
149 end
146 end
150 end
147
151
148 class WikiAnnotate
152 class WikiAnnotate
149 attr_reader :lines, :content
153 attr_reader :lines, :content
150
154
151 def initialize(content)
155 def initialize(content)
152 @content = content
156 @content = content
153 current = content
157 current = content
154 current_lines = current.text.split(/\r?\n/)
158 current_lines = current.text.split(/\r?\n/)
155 @lines = current_lines.collect {|t| [nil, nil, t]}
159 @lines = current_lines.collect {|t| [nil, nil, t]}
156 positions = []
160 positions = []
157 current_lines.size.times {|i| positions << i}
161 current_lines.size.times {|i| positions << i}
158 while (current.previous)
162 while (current.previous)
159 d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
163 d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
160 d.each_slice(3) do |s|
164 d.each_slice(3) do |s|
161 sign, line = s[0], s[1]
165 sign, line = s[0], s[1]
162 if sign == '+' && positions[line] && positions[line] != -1
166 if sign == '+' && positions[line] && positions[line] != -1
163 if @lines[positions[line]][0].nil?
167 if @lines[positions[line]][0].nil?
164 @lines[positions[line]][0] = current.version
168 @lines[positions[line]][0] = current.version
165 @lines[positions[line]][1] = current.author
169 @lines[positions[line]][1] = current.author
166 end
170 end
167 end
171 end
168 end
172 end
169 d.each_slice(3) do |s|
173 d.each_slice(3) do |s|
170 sign, line = s[0], s[1]
174 sign, line = s[0], s[1]
171 if sign == '-'
175 if sign == '-'
172 positions.insert(line, -1)
176 positions.insert(line, -1)
173 else
177 else
174 positions[line] = nil
178 positions[line] = nil
175 end
179 end
176 end
180 end
177 positions.compact!
181 positions.compact!
178 # Stop if every line is annotated
182 # Stop if every line is annotated
179 break unless @lines.detect { |line| line[0].nil? }
183 break unless @lines.detect { |line| line[0].nil? }
180 current = current.previous
184 current = current.previous
181 end
185 end
182 @lines.each { |line| line[0] ||= current.version }
186 @lines.each { |line| line[0] ||= current.version }
183 end
187 end
184 end
188 end
@@ -1,18 +1,18
1 <div class="attachments">
1 <div class="attachments">
2 <% for attachment in attachments %>
2 <% for attachment in attachments %>
3 <p><%= link_to_attachment attachment, :class => 'icon icon-attachment' -%>
3 <p><%= link_to_attachment attachment, :class => 'icon icon-attachment' -%>
4 <%= h(" - #{attachment.description}") unless attachment.description.blank? %>
4 <%= h(" - #{attachment.description}") unless attachment.description.blank? %>
5 <span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
5 <span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
6 <% if options[:delete_url] %>
6 <% if options[:deletable] %>
7 <%= link_to image_tag('delete.png'), options[:delete_url].update({:attachment_id => attachment}),
7 <%= link_to image_tag('delete.png'), {:controller => 'attachments', :action => 'destroy', :id => attachment},
8 :confirm => l(:text_are_you_sure),
8 :confirm => l(:text_are_you_sure),
9 :method => :post,
9 :method => :post,
10 :class => 'delete',
10 :class => 'delete',
11 :title => l(:button_delete) %>
11 :title => l(:button_delete) %>
12 <% end %>
12 <% end %>
13 <% unless options[:no_author] %>
13 <% if options[:author] %>
14 <span class="author"><%= attachment.author %>, <%= format_time(attachment.created_on) %></span>
14 <span class="author"><%= attachment.author %>, <%= format_time(attachment.created_on) %></span>
15 <% end %>
15 <% end %>
16 </p>
16 </p>
17 <% end %>
17 <% end %>
18 </div>
18 </div>
@@ -1,28 +1,28
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to_if_authorized l(:button_edit), {:controller => 'documents', :action => 'edit', :id => @document}, :class => 'icon icon-edit', :accesskey => accesskey(:edit) %>
2 <%= link_to_if_authorized l(:button_edit), {:controller => 'documents', :action => 'edit', :id => @document}, :class => 'icon icon-edit', :accesskey => accesskey(:edit) %>
3 <%= link_to_if_authorized l(:button_delete), {:controller => 'documents', :action => 'destroy', :id => @document}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
3 <%= link_to_if_authorized l(:button_delete), {:controller => 'documents', :action => 'destroy', :id => @document}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
4 </div>
4 </div>
5
5
6 <h2><%=h @document.title %></h2>
6 <h2><%=h @document.title %></h2>
7
7
8 <p><em><%=h @document.category.name %><br />
8 <p><em><%=h @document.category.name %><br />
9 <%= format_date @document.created_on %></em></p>
9 <%= format_date @document.created_on %></em></p>
10 <div class="wiki">
10 <div class="wiki">
11 <%= textilizable @document.description, :attachments => @document.attachments %>
11 <%= textilizable @document.description, :attachments => @document.attachments %>
12 </div>
12 </div>
13
13
14 <h3><%= l(:label_attachment_plural) %></h3>
14 <h3><%= l(:label_attachment_plural) %></h3>
15 <%= link_to_attachments @attachments, :delete_url => (authorize_for('documents', 'destroy_attachment') ? {:controller => 'documents', :action => 'destroy_attachment', :id => @document} : nil) %>
15 <%= link_to_attachments @document %>
16
16
17 <% if authorize_for('documents', 'add_attachment') %>
17 <% if authorize_for('documents', 'add_attachment') %>
18 <p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
18 <p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
19 :id => 'attach_files_link' %></p>
19 :id => 'attach_files_link' %></p>
20 <% form_tag({ :controller => 'documents', :action => 'add_attachment', :id => @document }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %>
20 <% form_tag({ :controller => 'documents', :action => 'add_attachment', :id => @document }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %>
21 <div class="box">
21 <div class="box">
22 <p><%= render :partial => 'attachments/form' %></p>
22 <p><%= render :partial => 'attachments/form' %></p>
23 </div>
23 </div>
24 <%= submit_tag l(:button_add) %>
24 <%= submit_tag l(:button_add) %>
25 <% end %>
25 <% end %>
26 <% end %>
26 <% end %>
27
27
28 <% html_title @document.title -%>
28 <% html_title @document.title -%>
@@ -1,128 +1,126
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
2 <%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
3 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %>
3 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %>
4 <%= watcher_tag(@issue, User.current) %>
4 <%= watcher_tag(@issue, User.current) %>
5 <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %>
5 <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %>
6 <%= link_to_if_authorized l(:button_move), {:controller => 'issues', :action => 'move', :id => @issue }, :class => 'icon icon-move' %>
6 <%= link_to_if_authorized l(:button_move), {:controller => 'issues', :action => 'move', :id => @issue }, :class => 'icon icon-move' %>
7 <%= 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' %>
7 <%= 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' %>
8 </div>
8 </div>
9
9
10 <h2><%= @issue.tracker.name %> #<%= @issue.id %></h2>
10 <h2><%= @issue.tracker.name %> #<%= @issue.id %></h2>
11
11
12 <div class="issue <%= "status-#{@issue.status.position} priority-#{@issue.priority.position}" %>">
12 <div class="issue <%= "status-#{@issue.status.position} priority-#{@issue.priority.position}" %>">
13 <%= avatar(@issue.author, :size => "64") %>
13 <%= avatar(@issue.author, :size => "64") %>
14 <h3><%=h @issue.subject %></h3>
14 <h3><%=h @issue.subject %></h3>
15 <p class="author">
15 <p class="author">
16 <%= authoring @issue.created_on, @issue.author %>.
16 <%= authoring @issue.created_on, @issue.author %>.
17 <%= l(:label_updated_time, distance_of_time_in_words(Time.now, @issue.updated_on)) + '.' if @issue.created_on != @issue.updated_on %>
17 <%= l(:label_updated_time, distance_of_time_in_words(Time.now, @issue.updated_on)) + '.' if @issue.created_on != @issue.updated_on %>
18 </p>
18 </p>
19
19
20 <table width="100%">
20 <table width="100%">
21 <tr>
21 <tr>
22 <td style="width:15%" class="status"><b><%=l(:field_status)%>:</b></td><td style="width:35%" class="status status-<%= @issue.status.name %>"><%= @issue.status.name %></td>
22 <td style="width:15%" class="status"><b><%=l(:field_status)%>:</b></td><td style="width:35%" class="status status-<%= @issue.status.name %>"><%= @issue.status.name %></td>
23 <td style="width:15%" class="start-date"><b><%=l(:field_start_date)%>:</b></td><td style="width:35%"><%= format_date(@issue.start_date) %></td>
23 <td style="width:15%" class="start-date"><b><%=l(:field_start_date)%>:</b></td><td style="width:35%"><%= format_date(@issue.start_date) %></td>
24 </tr>
24 </tr>
25 <tr>
25 <tr>
26 <td class="priority"><b><%=l(:field_priority)%>:</b></td><td class="priority priority-<%= @issue.priority.name %>"><%= @issue.priority.name %></td>
26 <td class="priority"><b><%=l(:field_priority)%>:</b></td><td class="priority priority-<%= @issue.priority.name %>"><%= @issue.priority.name %></td>
27 <td class="due-date"><b><%=l(:field_due_date)%>:</b></td><td class="due-date"><%= format_date(@issue.due_date) %></td>
27 <td class="due-date"><b><%=l(:field_due_date)%>:</b></td><td class="due-date"><%= format_date(@issue.due_date) %></td>
28 </tr>
28 </tr>
29 <tr>
29 <tr>
30 <td class="assigned-to"><b><%=l(:field_assigned_to)%>:</b></td><td><%= avatar(@issue.assigned_to, :size => "14") %><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td>
30 <td class="assigned-to"><b><%=l(:field_assigned_to)%>:</b></td><td><%= avatar(@issue.assigned_to, :size => "14") %><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td>
31 <td class="progress"><b><%=l(:field_done_ratio)%>:</b></td><td class="progress"><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td>
31 <td class="progress"><b><%=l(:field_done_ratio)%>:</b></td><td class="progress"><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td>
32 </tr>
32 </tr>
33 <tr>
33 <tr>
34 <td class="category"><b><%=l(:field_category)%>:</b></td><td><%=h @issue.category ? @issue.category.name : "-" %></td>
34 <td class="category"><b><%=l(:field_category)%>:</b></td><td><%=h @issue.category ? @issue.category.name : "-" %></td>
35 <% if User.current.allowed_to?(:view_time_entries, @project) %>
35 <% if User.current.allowed_to?(:view_time_entries, @project) %>
36 <td class="spent-time"><b><%=l(:label_spent_time)%>:</b></td>
36 <td class="spent-time"><b><%=l(:label_spent_time)%>:</b></td>
37 <td class="spent-hours"><%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time') : "-" %></td>
37 <td class="spent-hours"><%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time') : "-" %></td>
38 <% end %>
38 <% end %>
39 </tr>
39 </tr>
40 <tr>
40 <tr>
41 <td class="fixed-version"><b><%=l(:field_fixed_version)%>:</b></td><td><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td>
41 <td class="fixed-version"><b><%=l(:field_fixed_version)%>:</b></td><td><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td>
42 <% if @issue.estimated_hours %>
42 <% if @issue.estimated_hours %>
43 <td class="estimated-hours"><b><%=l(:field_estimated_hours)%>:</b></td><td><%= lwr(:label_f_hour, @issue.estimated_hours) %></td>
43 <td class="estimated-hours"><b><%=l(:field_estimated_hours)%>:</b></td><td><%= lwr(:label_f_hour, @issue.estimated_hours) %></td>
44 <% end %>
44 <% end %>
45 </tr>
45 </tr>
46 <tr>
46 <tr>
47 <% n = 0 -%>
47 <% n = 0 -%>
48 <% @issue.custom_values.each do |value| -%>
48 <% @issue.custom_values.each do |value| -%>
49 <td valign="top"><b><%=h value.custom_field.name %>:</b></td><td valign="top"><%= simple_format(h(show_value(value))) %></td>
49 <td valign="top"><b><%=h value.custom_field.name %>:</b></td><td valign="top"><%= simple_format(h(show_value(value))) %></td>
50 <% n = n + 1
50 <% n = n + 1
51 if (n > 1)
51 if (n > 1)
52 n = 0 %>
52 n = 0 %>
53 </tr><tr>
53 </tr><tr>
54 <%end
54 <%end
55 end %>
55 end %>
56 </tr>
56 </tr>
57 <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
57 <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
58 </table>
58 </table>
59 <hr />
59 <hr />
60
60
61 <div class="contextual">
61 <div class="contextual">
62 <%= link_to_remote_if_authorized l(:button_quote), { :url => {:action => 'reply', :id => @issue} }, :class => 'icon icon-comment' %>
62 <%= link_to_remote_if_authorized l(:button_quote), { :url => {:action => 'reply', :id => @issue} }, :class => 'icon icon-comment' %>
63 </div>
63 </div>
64
64
65 <p><strong><%=l(:field_description)%></strong></p>
65 <p><strong><%=l(:field_description)%></strong></p>
66 <div class="wiki">
66 <div class="wiki">
67 <%= textilizable @issue, :description, :attachments => @issue.attachments %>
67 <%= textilizable @issue, :description, :attachments => @issue.attachments %>
68 </div>
68 </div>
69
69
70 <% if @issue.attachments.any? %>
70 <%= link_to_attachments @issue %>
71 <%= link_to_attachments @issue.attachments, :delete_url => (authorize_for('issues', 'destroy_attachment') ? {:controller => 'issues', :action => 'destroy_attachment', :id => @issue} : nil) %>
72 <% end %>
73
71
74 <% if authorize_for('issue_relations', 'new') || @issue.relations.any? %>
72 <% if authorize_for('issue_relations', 'new') || @issue.relations.any? %>
75 <hr />
73 <hr />
76 <div id="relations">
74 <div id="relations">
77 <%= render :partial => 'relations' %>
75 <%= render :partial => 'relations' %>
78 </div>
76 </div>
79 <% end %>
77 <% end %>
80
78
81 <% if User.current.allowed_to?(:add_issue_watchers, @project) ||
79 <% if User.current.allowed_to?(:add_issue_watchers, @project) ||
82 (@issue.watchers.any? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
80 (@issue.watchers.any? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
83 <hr />
81 <hr />
84 <div id="watchers">
82 <div id="watchers">
85 <%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
83 <%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
86 </div>
84 </div>
87 <% end %>
85 <% end %>
88
86
89 </div>
87 </div>
90
88
91 <% if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @project) %>
89 <% if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @project) %>
92 <div id="issue-changesets">
90 <div id="issue-changesets">
93 <h3><%=l(:label_associated_revisions)%></h3>
91 <h3><%=l(:label_associated_revisions)%></h3>
94 <%= render :partial => 'changesets', :locals => { :changesets => @issue.changesets} %>
92 <%= render :partial => 'changesets', :locals => { :changesets => @issue.changesets} %>
95 </div>
93 </div>
96 <% end %>
94 <% end %>
97
95
98 <% if @journals.any? %>
96 <% if @journals.any? %>
99 <div id="history">
97 <div id="history">
100 <h3><%=l(:label_history)%></h3>
98 <h3><%=l(:label_history)%></h3>
101 <%= render :partial => 'history', :locals => { :journals => @journals } %>
99 <%= render :partial => 'history', :locals => { :journals => @journals } %>
102 </div>
100 </div>
103 <% end %>
101 <% end %>
104 <div style="clear: both;"></div>
102 <div style="clear: both;"></div>
105
103
106 <% if authorize_for('issues', 'edit') %>
104 <% if authorize_for('issues', 'edit') %>
107 <div id="update" style="display:none;">
105 <div id="update" style="display:none;">
108 <h3><%= l(:button_update) %></h3>
106 <h3><%= l(:button_update) %></h3>
109 <%= render :partial => 'edit' %>
107 <%= render :partial => 'edit' %>
110 </div>
108 </div>
111 <% end %>
109 <% end %>
112
110
113 <p class="other-formats">
111 <p class="other-formats">
114 <%= l(:label_export_to) %>
112 <%= l(:label_export_to) %>
115 <span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
113 <span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
116 <span><%= link_to 'PDF', {:format => 'pdf'}, :class => 'pdf' %></span>
114 <span><%= link_to 'PDF', {:format => 'pdf'}, :class => 'pdf' %></span>
117 </p>
115 </p>
118
116
119 <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
117 <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
120
118
121 <% content_for :sidebar do %>
119 <% content_for :sidebar do %>
122 <%= render :partial => 'issues/sidebar' %>
120 <%= render :partial => 'issues/sidebar' %>
123 <% end %>
121 <% end %>
124
122
125 <% content_for :header_tags do %>
123 <% content_for :header_tags do %>
126 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
124 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
127 <%= stylesheet_link_tag 'scm' %>
125 <%= stylesheet_link_tag 'scm' %>
128 <% end %>
126 <% end %>
@@ -1,61 +1,61
1 <%= breadcrumb link_to(l(:label_board_plural), {:controller => 'boards', :action => 'index', :project_id => @project}),
1 <%= breadcrumb link_to(l(:label_board_plural), {:controller => 'boards', :action => 'index', :project_id => @project}),
2 link_to(h(@board.name), {:controller => 'boards', :action => 'show', :project_id => @project, :id => @board}) %>
2 link_to(h(@board.name), {:controller => 'boards', :action => 'show', :project_id => @project, :id => @board}) %>
3
3
4 <div class="contextual">
4 <div class="contextual">
5 <%= watcher_tag(@topic, User.current) %>
5 <%= watcher_tag(@topic, User.current) %>
6 <%= link_to_remote_if_authorized l(:button_quote), { :url => {:action => 'quote', :id => @topic} }, :class => 'icon icon-comment' %>
6 <%= link_to_remote_if_authorized l(:button_quote), { :url => {:action => 'quote', :id => @topic} }, :class => 'icon icon-comment' %>
7 <%= link_to(l(:button_edit), {:action => 'edit', :id => @topic}, :class => 'icon icon-edit') if @message.editable_by?(User.current) %>
7 <%= link_to(l(:button_edit), {:action => 'edit', :id => @topic}, :class => 'icon icon-edit') if @message.editable_by?(User.current) %>
8 <%= link_to(l(:button_delete), {:action => 'destroy', :id => @topic}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') if @message.destroyable_by?(User.current) %>
8 <%= link_to(l(:button_delete), {:action => 'destroy', :id => @topic}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') if @message.destroyable_by?(User.current) %>
9 </div>
9 </div>
10
10
11 <h2><%=h @topic.subject %></h2>
11 <h2><%=h @topic.subject %></h2>
12
12
13 <div class="message">
13 <div class="message">
14 <p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p>
14 <p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p>
15 <div class="wiki">
15 <div class="wiki">
16 <%= textilizable(@topic.content, :attachments => @topic.attachments) %>
16 <%= textilizable(@topic.content, :attachments => @topic.attachments) %>
17 </div>
17 </div>
18 <%= link_to_attachments @topic.attachments, :no_author => true %>
18 <%= link_to_attachments @topic, :author => false %>
19 </div>
19 </div>
20 <br />
20 <br />
21
21
22 <% unless @replies.empty? %>
22 <% unless @replies.empty? %>
23 <h3 class="icon22 icon22-comment"><%= l(:label_reply_plural) %></h3>
23 <h3 class="icon22 icon22-comment"><%= l(:label_reply_plural) %></h3>
24 <% @replies.each do |message| %>
24 <% @replies.each do |message| %>
25 <a name="<%= "message-#{message.id}" %>"></a>
25 <a name="<%= "message-#{message.id}" %>"></a>
26 <div class="contextual">
26 <div class="contextual">
27 <%= link_to_remote_if_authorized image_tag('comment.png'), { :url => {:action => 'quote', :id => message} }, :title => l(:button_quote) %>
27 <%= link_to_remote_if_authorized image_tag('comment.png'), { :url => {:action => 'quote', :id => message} }, :title => l(:button_quote) %>
28 <%= link_to(image_tag('edit.png'), {:action => 'edit', :id => message}, :title => l(:button_edit)) if message.editable_by?(User.current) %>
28 <%= link_to(image_tag('edit.png'), {:action => 'edit', :id => message}, :title => l(:button_edit)) if message.editable_by?(User.current) %>
29 <%= link_to(image_tag('delete.png'), {:action => 'destroy', :id => message}, :method => :post, :confirm => l(:text_are_you_sure), :title => l(:button_delete)) if message.destroyable_by?(User.current) %>
29 <%= link_to(image_tag('delete.png'), {:action => 'destroy', :id => message}, :method => :post, :confirm => l(:text_are_you_sure), :title => l(:button_delete)) if message.destroyable_by?(User.current) %>
30 </div>
30 </div>
31 <div class="message reply">
31 <div class="message reply">
32 <h4><%=h message.subject %> - <%= authoring message.created_on, message.author %></h4>
32 <h4><%=h message.subject %> - <%= authoring message.created_on, message.author %></h4>
33 <div class="wiki"><%= textilizable message, :content, :attachments => message.attachments %></div>
33 <div class="wiki"><%= textilizable message, :content, :attachments => message.attachments %></div>
34 <%= link_to_attachments message.attachments, :no_author => true %>
34 <%= link_to_attachments message, :author => false %>
35 </div>
35 </div>
36 <% end %>
36 <% end %>
37 <% end %>
37 <% end %>
38
38
39 <% if !@topic.locked? && authorize_for('messages', 'reply') %>
39 <% if !@topic.locked? && authorize_for('messages', 'reply') %>
40 <p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
40 <p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
41 <div id="reply" style="display:none;">
41 <div id="reply" style="display:none;">
42 <% form_for :reply, @reply, :url => {:action => 'reply', :id => @topic}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
42 <% form_for :reply, @reply, :url => {:action => 'reply', :id => @topic}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
43 <%= render :partial => 'form', :locals => {:f => f, :replying => true} %>
43 <%= render :partial => 'form', :locals => {:f => f, :replying => true} %>
44 <%= submit_tag l(:button_submit) %>
44 <%= submit_tag l(:button_submit) %>
45 <%= link_to_remote l(:label_preview),
45 <%= link_to_remote l(:label_preview),
46 { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
46 { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
47 :method => 'post',
47 :method => 'post',
48 :update => 'preview',
48 :update => 'preview',
49 :with => "Form.serialize('message-form')",
49 :with => "Form.serialize('message-form')",
50 :complete => "Element.scrollTo('preview')"
50 :complete => "Element.scrollTo('preview')"
51 }, :accesskey => accesskey(:preview) %>
51 }, :accesskey => accesskey(:preview) %>
52 <% end %>
52 <% end %>
53 <div id="preview" class="wiki"></div>
53 <div id="preview" class="wiki"></div>
54 </div>
54 </div>
55 <% end %>
55 <% end %>
56
56
57 <% content_for :header_tags do %>
57 <% content_for :header_tags do %>
58 <%= stylesheet_link_tag 'scm' %>
58 <%= stylesheet_link_tag 'scm' %>
59 <% end %>
59 <% end %>
60
60
61 <% html_title h(@topic.subject) %>
61 <% html_title h(@topic.subject) %>
@@ -1,44 +1,45
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to_if_authorized l(:label_attachment_new), {:controller => 'projects', :action => 'add_file', :id => @project}, :class => 'icon icon-add' %>
2 <%= link_to_if_authorized l(:label_attachment_new), {:controller => 'projects', :action => 'add_file', :id => @project}, :class => 'icon icon-add' %>
3 </div>
3 </div>
4
4
5 <h2><%=l(:label_attachment_plural)%></h2>
5 <h2><%=l(:label_attachment_plural)%></h2>
6
6
7 <% delete_allowed = authorize_for('versions', 'destroy_file') %>
7 <% delete_allowed = User.current.allowed_to?(:manage_files, @project) %>
8
8
9 <table class="list">
9 <table class="list">
10 <thead><tr>
10 <thead><tr>
11 <th><%=l(:field_version)%></th>
11 <th><%=l(:field_version)%></th>
12 <%= sort_header_tag("#{Attachment.table_name}.filename", :caption => l(:field_filename)) %>
12 <%= sort_header_tag("#{Attachment.table_name}.filename", :caption => l(:field_filename)) %>
13 <%= sort_header_tag("#{Attachment.table_name}.created_on", :caption => l(:label_date), :default_order => 'desc') %>
13 <%= sort_header_tag("#{Attachment.table_name}.created_on", :caption => l(:label_date), :default_order => 'desc') %>
14 <%= sort_header_tag("#{Attachment.table_name}.filesize", :caption => l(:field_filesize), :default_order => 'desc') %>
14 <%= sort_header_tag("#{Attachment.table_name}.filesize", :caption => l(:field_filesize), :default_order => 'desc') %>
15 <%= sort_header_tag("#{Attachment.table_name}.downloads", :caption => l(:label_downloads_abbr), :default_order => 'desc') %>
15 <%= sort_header_tag("#{Attachment.table_name}.downloads", :caption => l(:label_downloads_abbr), :default_order => 'desc') %>
16 <th>MD5</th>
16 <th>MD5</th>
17 <% if delete_allowed %><th></th><% end %>
17 <% if delete_allowed %><th></th><% end %>
18 </tr></thead>
18 </tr></thead>
19 <tbody>
19 <tbody>
20 <% for version in @versions %>
20 <% for version in @versions %>
21 <% unless version.attachments.empty? %>
21 <% unless version.attachments.empty? %>
22 <tr><th colspan="7" align="left"><span class="icon icon-package"><b><%= version.name %></b></span></th></tr>
22 <tr><th colspan="7" align="left"><span class="icon icon-package"><b><%= version.name %></b></span></th></tr>
23 <% for file in version.attachments %>
23 <% for file in version.attachments %>
24 <tr class="<%= cycle("odd", "even") %>">
24 <tr class="<%= cycle("odd", "even") %>">
25 <td></td>
25 <td></td>
26 <td><%= link_to_attachment file, :download => true, :title => file.description %></td>
26 <td><%= link_to_attachment file, :download => true, :title => file.description %></td>
27 <td align="center"><%= format_time(file.created_on) %></td>
27 <td align="center"><%= format_time(file.created_on) %></td>
28 <td align="center"><%= number_to_human_size(file.filesize) %></td>
28 <td align="center"><%= number_to_human_size(file.filesize) %></td>
29 <td align="center"><%= file.downloads %></td>
29 <td align="center"><%= file.downloads %></td>
30 <td align="center"><small><%= file.digest %></small></td>
30 <td align="center"><small><%= file.digest %></small></td>
31 <% if delete_allowed %>
31 <% if delete_allowed %>
32 <td align="center">
32 <td align="center">
33 <%= link_to_if_authorized image_tag('delete.png'), {:controller => 'versions', :action => 'destroy_file', :id => version, :attachment_id => file}, :confirm => l(:text_are_you_sure), :method => :post %>
33 <%= link_to image_tag('delete.png'), {:controller => 'attachments', :action => 'destroy', :id => file},
34 :confirm => l(:text_are_you_sure), :method => :post %>
34 </td>
35 </td>
35 <% end %>
36 <% end %>
36 </tr>
37 </tr>
37 <% end
38 <% end
38 reset_cycle %>
39 reset_cycle %>
39 <% end %>
40 <% end %>
40 <% end %>
41 <% end %>
41 </tbody>
42 </tbody>
42 </table>
43 </table>
43
44
44 <% html_title(l(:label_attachment_plural)) -%>
45 <% html_title(l(:label_attachment_plural)) -%>
@@ -1,59 +1,59
1 <div class="contextual">
1 <div class="contextual">
2 <% if @editable %>
2 <% if @editable %>
3 <%= link_to_if_authorized(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit', :accesskey => accesskey(:edit)) if @content.version == @page.content.version %>
3 <%= link_to_if_authorized(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit', :accesskey => accesskey(:edit)) if @content.version == @page.content.version %>
4 <%= link_to_if_authorized(l(:button_lock), {:action => 'protect', :page => @page.title, :protected => 1}, :method => :post, :class => 'icon icon-lock') if !@page.protected? %>
4 <%= link_to_if_authorized(l(:button_lock), {:action => 'protect', :page => @page.title, :protected => 1}, :method => :post, :class => 'icon icon-lock') if !@page.protected? %>
5 <%= link_to_if_authorized(l(:button_unlock), {:action => 'protect', :page => @page.title, :protected => 0}, :method => :post, :class => 'icon icon-unlock') if @page.protected? %>
5 <%= link_to_if_authorized(l(:button_unlock), {:action => 'protect', :page => @page.title, :protected => 0}, :method => :post, :class => 'icon icon-unlock') if @page.protected? %>
6 <%= link_to_if_authorized(l(:button_rename), {:action => 'rename', :page => @page.title}, :class => 'icon icon-move') if @content.version == @page.content.version %>
6 <%= link_to_if_authorized(l(:button_rename), {:action => 'rename', :page => @page.title}, :class => 'icon icon-move') if @content.version == @page.content.version %>
7 <%= link_to_if_authorized(l(:button_delete), {:action => 'destroy', :page => @page.title}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') %>
7 <%= link_to_if_authorized(l(:button_delete), {:action => 'destroy', :page => @page.title}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') %>
8 <%= link_to_if_authorized(l(:button_rollback), {:action => 'edit', :page => @page.title, :version => @content.version }, :class => 'icon icon-cancel') if @content.version < @page.content.version %>
8 <%= link_to_if_authorized(l(:button_rollback), {:action => 'edit', :page => @page.title, :version => @content.version }, :class => 'icon icon-cancel') if @content.version < @page.content.version %>
9 <% end %>
9 <% end %>
10 <%= link_to_if_authorized(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %>
10 <%= link_to_if_authorized(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %>
11 </div>
11 </div>
12
12
13 <%= breadcrumb(@page.ancestors.reverse.collect {|parent| link_to h(parent.pretty_title), {:page => parent.title}}) %>
13 <%= breadcrumb(@page.ancestors.reverse.collect {|parent| link_to h(parent.pretty_title), {:page => parent.title}}) %>
14
14
15 <% if @content.version != @page.content.version %>
15 <% if @content.version != @page.content.version %>
16 <p>
16 <p>
17 <%= link_to(('&#171; ' + l(:label_previous)), :action => 'index', :page => @page.title, :version => (@content.version - 1)) + " - " if @content.version > 1 %>
17 <%= link_to(('&#171; ' + l(:label_previous)), :action => 'index', :page => @page.title, :version => (@content.version - 1)) + " - " if @content.version > 1 %>
18 <%= "#{l(:label_version)} #{@content.version}/#{@page.content.version}" %>
18 <%= "#{l(:label_version)} #{@content.version}/#{@page.content.version}" %>
19 <%= '(' + link_to('diff', :controller => 'wiki', :action => 'diff', :page => @page.title, :version => @content.version) + ')' if @content.version > 1 %> -
19 <%= '(' + link_to('diff', :controller => 'wiki', :action => 'diff', :page => @page.title, :version => @content.version) + ')' if @content.version > 1 %> -
20 <%= link_to((l(:label_next) + ' &#187;'), :action => 'index', :page => @page.title, :version => (@content.version + 1)) + " - " if @content.version < @page.content.version %>
20 <%= link_to((l(:label_next) + ' &#187;'), :action => 'index', :page => @page.title, :version => (@content.version + 1)) + " - " if @content.version < @page.content.version %>
21 <%= link_to(l(:label_current_version), :action => 'index', :page => @page.title) %>
21 <%= link_to(l(:label_current_version), :action => 'index', :page => @page.title) %>
22 <br />
22 <br />
23 <em><%= @content.author ? @content.author.name : "anonyme" %>, <%= format_time(@content.updated_on) %> </em><br />
23 <em><%= @content.author ? @content.author.name : "anonyme" %>, <%= format_time(@content.updated_on) %> </em><br />
24 <%=h @content.comments %>
24 <%=h @content.comments %>
25 </p>
25 </p>
26 <hr />
26 <hr />
27 <% end %>
27 <% end %>
28
28
29 <%= render(:partial => "wiki/content", :locals => {:content => @content}) %>
29 <%= render(:partial => "wiki/content", :locals => {:content => @content}) %>
30
30
31 <%= link_to_attachments @page.attachments, :delete_url => ((@editable && authorize_for('wiki', 'destroy_attachment')) ? {:controller => 'wiki', :action => 'destroy_attachment', :page => @page.title} : nil) %>
31 <%= link_to_attachments @page %>
32
32
33 <% if @editable && authorize_for('wiki', 'add_attachment') %>
33 <% if @editable && authorize_for('wiki', 'add_attachment') %>
34 <p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
34 <p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
35 :id => 'attach_files_link' %></p>
35 :id => 'attach_files_link' %></p>
36 <% form_tag({ :controller => 'wiki', :action => 'add_attachment', :page => @page.title }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %>
36 <% form_tag({ :controller => 'wiki', :action => 'add_attachment', :page => @page.title }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %>
37 <div class="box">
37 <div class="box">
38 <p><%= render :partial => 'attachments/form' %></p>
38 <p><%= render :partial => 'attachments/form' %></p>
39 </div>
39 </div>
40 <%= submit_tag l(:button_add) %>
40 <%= submit_tag l(:button_add) %>
41 <%= link_to l(:button_cancel), {}, :onclick => "Element.hide('add_attachment_form'); Element.show('attach_files_link'); return false;" %>
41 <%= link_to l(:button_cancel), {}, :onclick => "Element.hide('add_attachment_form'); Element.show('attach_files_link'); return false;" %>
42 <% end %>
42 <% end %>
43 <% end %>
43 <% end %>
44
44
45 <p class="other-formats">
45 <p class="other-formats">
46 <%= l(:label_export_to) %>
46 <%= l(:label_export_to) %>
47 <span><%= link_to 'HTML', {:page => @page.title, :export => 'html', :version => @content.version}, :class => 'html' %></span>
47 <span><%= link_to 'HTML', {:page => @page.title, :export => 'html', :version => @content.version}, :class => 'html' %></span>
48 <span><%= link_to 'TXT', {:page => @page.title, :export => 'txt', :version => @content.version}, :class => 'text' %></span>
48 <span><%= link_to 'TXT', {:page => @page.title, :export => 'txt', :version => @content.version}, :class => 'text' %></span>
49 </p>
49 </p>
50
50
51 <% content_for :header_tags do %>
51 <% content_for :header_tags do %>
52 <%= stylesheet_link_tag 'scm' %>
52 <%= stylesheet_link_tag 'scm' %>
53 <% end %>
53 <% end %>
54
54
55 <% content_for :sidebar do %>
55 <% content_for :sidebar do %>
56 <%= render :partial => 'sidebar' %>
56 <%= render :partial => 'sidebar' %>
57 <% end %>
57 <% end %>
58
58
59 <% html_title @page.pretty_title %>
59 <% html_title @page.pretty_title %>
@@ -1,163 +1,163
1 require 'redmine/access_control'
1 require 'redmine/access_control'
2 require 'redmine/menu_manager'
2 require 'redmine/menu_manager'
3 require 'redmine/activity'
3 require 'redmine/activity'
4 require 'redmine/mime_type'
4 require 'redmine/mime_type'
5 require 'redmine/core_ext'
5 require 'redmine/core_ext'
6 require 'redmine/themes'
6 require 'redmine/themes'
7 require 'redmine/hook'
7 require 'redmine/hook'
8 require 'redmine/plugin'
8 require 'redmine/plugin'
9 require 'redmine/wiki_formatting'
9 require 'redmine/wiki_formatting'
10
10
11 begin
11 begin
12 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
12 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
13 rescue LoadError
13 rescue LoadError
14 # RMagick is not available
14 # RMagick is not available
15 end
15 end
16
16
17 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git Filesystem )
17 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git Filesystem )
18
18
19 # Permissions
19 # Permissions
20 Redmine::AccessControl.map do |map|
20 Redmine::AccessControl.map do |map|
21 map.permission :view_project, {:projects => [:show, :activity]}, :public => true
21 map.permission :view_project, {:projects => [:show, :activity]}, :public => true
22 map.permission :search_project, {:search => :index}, :public => true
22 map.permission :search_project, {:search => :index}, :public => true
23 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
23 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
24 map.permission :select_project_modules, {:projects => :modules}, :require => :member
24 map.permission :select_project_modules, {:projects => :modules}, :require => :member
25 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy]}, :require => :member
25 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy]}, :require => :member
26 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member
26 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member
27
27
28 map.project_module :issue_tracking do |map|
28 map.project_module :issue_tracking do |map|
29 # Issue categories
29 # Issue categories
30 map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
30 map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
31 # Issues
31 # Issues
32 map.permission :view_issues, {:projects => [:changelog, :roadmap],
32 map.permission :view_issues, {:projects => [:changelog, :roadmap],
33 :issues => [:index, :changes, :show, :context_menu],
33 :issues => [:index, :changes, :show, :context_menu],
34 :versions => [:show, :status_by],
34 :versions => [:show, :status_by],
35 :queries => :index,
35 :queries => :index,
36 :reports => :issue_report}, :public => true
36 :reports => :issue_report}, :public => true
37 map.permission :add_issues, {:issues => :new}
37 map.permission :add_issues, {:issues => :new}
38 map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit, :destroy_attachment]}
38 map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit]}
39 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
39 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
40 map.permission :add_issue_notes, {:issues => [:edit, :reply]}
40 map.permission :add_issue_notes, {:issues => [:edit, :reply]}
41 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
41 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
42 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
42 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
43 map.permission :move_issues, {:issues => :move}, :require => :loggedin
43 map.permission :move_issues, {:issues => :move}, :require => :loggedin
44 map.permission :delete_issues, {:issues => :destroy}, :require => :member
44 map.permission :delete_issues, {:issues => :destroy}, :require => :member
45 # Queries
45 # Queries
46 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
46 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
47 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
47 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
48 # Gantt & calendar
48 # Gantt & calendar
49 map.permission :view_gantt, :issues => :gantt
49 map.permission :view_gantt, :issues => :gantt
50 map.permission :view_calendar, :issues => :calendar
50 map.permission :view_calendar, :issues => :calendar
51 # Watchers
51 # Watchers
52 map.permission :view_issue_watchers, {}
52 map.permission :view_issue_watchers, {}
53 map.permission :add_issue_watchers, {:watchers => :new}
53 map.permission :add_issue_watchers, {:watchers => :new}
54 end
54 end
55
55
56 map.project_module :time_tracking do |map|
56 map.project_module :time_tracking do |map|
57 map.permission :log_time, {:timelog => :edit}, :require => :loggedin
57 map.permission :log_time, {:timelog => :edit}, :require => :loggedin
58 map.permission :view_time_entries, :timelog => [:details, :report]
58 map.permission :view_time_entries, :timelog => [:details, :report]
59 map.permission :edit_time_entries, {:timelog => [:edit, :destroy]}, :require => :member
59 map.permission :edit_time_entries, {:timelog => [:edit, :destroy]}, :require => :member
60 map.permission :edit_own_time_entries, {:timelog => [:edit, :destroy]}, :require => :loggedin
60 map.permission :edit_own_time_entries, {:timelog => [:edit, :destroy]}, :require => :loggedin
61 end
61 end
62
62
63 map.project_module :news do |map|
63 map.project_module :news do |map|
64 map.permission :manage_news, {:news => [:new, :edit, :destroy, :destroy_comment]}, :require => :member
64 map.permission :manage_news, {:news => [:new, :edit, :destroy, :destroy_comment]}, :require => :member
65 map.permission :view_news, {:news => [:index, :show]}, :public => true
65 map.permission :view_news, {:news => [:index, :show]}, :public => true
66 map.permission :comment_news, {:news => :add_comment}
66 map.permission :comment_news, {:news => :add_comment}
67 end
67 end
68
68
69 map.project_module :documents do |map|
69 map.project_module :documents do |map|
70 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment, :destroy_attachment]}, :require => :loggedin
70 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment]}, :require => :loggedin
71 map.permission :view_documents, :documents => [:index, :show, :download]
71 map.permission :view_documents, :documents => [:index, :show, :download]
72 end
72 end
73
73
74 map.project_module :files do |map|
74 map.project_module :files do |map|
75 map.permission :manage_files, {:projects => :add_file, :versions => :destroy_file}, :require => :loggedin
75 map.permission :manage_files, {:projects => :add_file}, :require => :loggedin
76 map.permission :view_files, :projects => :list_files, :versions => :download
76 map.permission :view_files, :projects => :list_files, :versions => :download
77 end
77 end
78
78
79 map.project_module :wiki do |map|
79 map.project_module :wiki do |map|
80 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
80 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
81 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
81 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
82 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
82 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
83 map.permission :view_wiki_pages, :wiki => [:index, :special]
83 map.permission :view_wiki_pages, :wiki => [:index, :special]
84 map.permission :view_wiki_edits, :wiki => [:history, :diff, :annotate]
84 map.permission :view_wiki_edits, :wiki => [:history, :diff, :annotate]
85 map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment]
85 map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment]
86 map.permission :delete_wiki_pages_attachments, :wiki => :destroy_attachment
86 map.permission :delete_wiki_pages_attachments, {}
87 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
87 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
88 end
88 end
89
89
90 map.project_module :repository do |map|
90 map.project_module :repository do |map|
91 map.permission :manage_repository, {:repositories => [:edit, :committers, :destroy]}, :require => :member
91 map.permission :manage_repository, {:repositories => [:edit, :committers, :destroy]}, :require => :member
92 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
92 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
93 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
93 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
94 map.permission :commit_access, {}
94 map.permission :commit_access, {}
95 end
95 end
96
96
97 map.project_module :boards do |map|
97 map.project_module :boards do |map|
98 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
98 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
99 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
99 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
100 map.permission :add_messages, {:messages => [:new, :reply, :quote]}
100 map.permission :add_messages, {:messages => [:new, :reply, :quote]}
101 map.permission :edit_messages, {:messages => :edit}, :require => :member
101 map.permission :edit_messages, {:messages => :edit}, :require => :member
102 map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
102 map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
103 map.permission :delete_messages, {:messages => :destroy}, :require => :member
103 map.permission :delete_messages, {:messages => :destroy}, :require => :member
104 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
104 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
105 end
105 end
106 end
106 end
107
107
108 Redmine::MenuManager.map :top_menu do |menu|
108 Redmine::MenuManager.map :top_menu do |menu|
109 menu.push :home, :home_path
109 menu.push :home, :home_path
110 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
110 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
111 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
111 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
112 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
112 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
113 menu.push :help, Redmine::Info.help_url, :last => true
113 menu.push :help, Redmine::Info.help_url, :last => true
114 end
114 end
115
115
116 Redmine::MenuManager.map :account_menu do |menu|
116 Redmine::MenuManager.map :account_menu do |menu|
117 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
117 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
118 menu.push :register, { :controller => 'account', :action => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
118 menu.push :register, { :controller => 'account', :action => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
119 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
119 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
120 menu.push :logout, :signout_path, :if => Proc.new { User.current.logged? }
120 menu.push :logout, :signout_path, :if => Proc.new { User.current.logged? }
121 end
121 end
122
122
123 Redmine::MenuManager.map :application_menu do |menu|
123 Redmine::MenuManager.map :application_menu do |menu|
124 # Empty
124 # Empty
125 end
125 end
126
126
127 Redmine::MenuManager.map :admin_menu do |menu|
127 Redmine::MenuManager.map :admin_menu do |menu|
128 # Empty
128 # Empty
129 end
129 end
130
130
131 Redmine::MenuManager.map :project_menu do |menu|
131 Redmine::MenuManager.map :project_menu do |menu|
132 menu.push :overview, { :controller => 'projects', :action => 'show' }
132 menu.push :overview, { :controller => 'projects', :action => 'show' }
133 menu.push :activity, { :controller => 'projects', :action => 'activity' }
133 menu.push :activity, { :controller => 'projects', :action => 'activity' }
134 menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' },
134 menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' },
135 :if => Proc.new { |p| p.versions.any? }
135 :if => Proc.new { |p| p.versions.any? }
136 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
136 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
137 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
137 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
138 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
138 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
139 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
139 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
140 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
140 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
141 menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil },
141 menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil },
142 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
142 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
143 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
143 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
144 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
144 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
145 menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
145 menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
146 menu.push :repository, { :controller => 'repositories', :action => 'show' },
146 menu.push :repository, { :controller => 'repositories', :action => 'show' },
147 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
147 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
148 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
148 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
149 end
149 end
150
150
151 Redmine::Activity.map do |activity|
151 Redmine::Activity.map do |activity|
152 activity.register :issues, :class_name => %w(Issue Journal)
152 activity.register :issues, :class_name => %w(Issue Journal)
153 activity.register :changesets
153 activity.register :changesets
154 activity.register :news
154 activity.register :news
155 activity.register :documents, :class_name => %w(Document Attachment)
155 activity.register :documents, :class_name => %w(Document Attachment)
156 activity.register :files, :class_name => 'Attachment'
156 activity.register :files, :class_name => 'Attachment'
157 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
157 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
158 activity.register :messages, :default => false
158 activity.register :messages, :default => false
159 end
159 end
160
160
161 Redmine::WikiFormatting.map do |format|
161 Redmine::WikiFormatting.map do |format|
162 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
162 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
163 end
163 end
@@ -1,79 +1,108
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'attachments_controller'
19 require 'attachments_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class AttachmentsController; def rescue_action(e) raise e end; end
22 class AttachmentsController; def rescue_action(e) raise e end; end
23
23
24
24
25 class AttachmentsControllerTest < Test::Unit::TestCase
25 class AttachmentsControllerTest < Test::Unit::TestCase
26 fixtures :users, :projects, :roles, :members, :enabled_modules, :issues, :attachments
26 fixtures :users, :projects, :roles, :members, :enabled_modules, :issues, :attachments
27
27
28 def setup
28 def setup
29 @controller = AttachmentsController.new
29 @controller = AttachmentsController.new
30 @request = ActionController::TestRequest.new
30 @request = ActionController::TestRequest.new
31 @response = ActionController::TestResponse.new
31 @response = ActionController::TestResponse.new
32 Attachment.storage_path = "#{RAILS_ROOT}/test/fixtures/files"
32 Attachment.storage_path = "#{RAILS_ROOT}/test/fixtures/files"
33 User.current = nil
33 User.current = nil
34 end
34 end
35
35
36 def test_routing
36 def test_routing
37 assert_routing('/attachments/1', :controller => 'attachments', :action => 'show', :id => '1')
37 assert_routing('/attachments/1', :controller => 'attachments', :action => 'show', :id => '1')
38 assert_routing('/attachments/1/filename.ext', :controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext')
38 assert_routing('/attachments/1/filename.ext', :controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext')
39 assert_routing('/attachments/download/1', :controller => 'attachments', :action => 'download', :id => '1')
39 assert_routing('/attachments/download/1', :controller => 'attachments', :action => 'download', :id => '1')
40 assert_routing('/attachments/download/1/filename.ext', :controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext')
40 assert_routing('/attachments/download/1/filename.ext', :controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext')
41 end
41 end
42
42
43 def test_recognizes
43 def test_recognizes
44 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/1')
44 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/1')
45 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/show/1')
45 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/show/1')
46 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext'}, '/attachments/1/filename.ext')
46 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext'}, '/attachments/1/filename.ext')
47 assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1'}, '/attachments/download/1')
47 assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1'}, '/attachments/download/1')
48 assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext'},'/attachments/download/1/filename.ext')
48 assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext'},'/attachments/download/1/filename.ext')
49 end
49 end
50
50
51 def test_show_diff
51 def test_show_diff
52 get :show, :id => 5
52 get :show, :id => 5
53 assert_response :success
53 assert_response :success
54 assert_template 'diff'
54 assert_template 'diff'
55 end
55 end
56
56
57 def test_show_text_file
57 def test_show_text_file
58 get :show, :id => 4
58 get :show, :id => 4
59 assert_response :success
59 assert_response :success
60 assert_template 'file'
60 assert_template 'file'
61 end
61 end
62
62
63 def test_show_other
63 def test_show_other
64 get :show, :id => 6
64 get :show, :id => 6
65 assert_response :success
65 assert_response :success
66 assert_equal 'application/octet-stream', @response.content_type
66 assert_equal 'application/octet-stream', @response.content_type
67 end
67 end
68
68
69 def test_download_text_file
69 def test_download_text_file
70 get :download, :id => 4
70 get :download, :id => 4
71 assert_response :success
71 assert_response :success
72 assert_equal 'application/x-ruby', @response.content_type
72 assert_equal 'application/x-ruby', @response.content_type
73 end
73 end
74
74
75 def test_anonymous_on_private_private
75 def test_anonymous_on_private_private
76 get :download, :id => 7
76 get :download, :id => 7
77 assert_redirected_to 'account/login'
77 assert_redirected_to 'account/login'
78 end
78 end
79
80 def test_destroy_issue_attachment
81 issue = Issue.find(3)
82 @request.session[:user_id] = 2
83
84 assert_difference 'issue.attachments.count', -1 do
85 post :destroy, :id => 1
86 end
87 # no referrer
88 assert_redirected_to 'projects/show/ecookbook'
89 assert_nil Attachment.find_by_id(1)
90 j = issue.journals.find(:first, :order => 'created_on DESC')
91 assert_equal 'attachment', j.details.first.property
92 assert_equal '1', j.details.first.prop_key
93 assert_equal 'error281.txt', j.details.first.old_value
94 end
95
96 def test_destroy_wiki_page_attachment
97 @request.session[:user_id] = 2
98 assert_difference 'Attachment.count', -1 do
99 post :destroy, :id => 3
100 end
101 end
102
103 def test_destroy_without_permission
104 post :destroy, :id => 3
105 assert_redirected_to '/login'
106 assert Attachment.find_by_id(3)
107 end
79 end
108 end
@@ -1,729 +1,716
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'issues_controller'
19 require 'issues_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class IssuesController; def rescue_action(e) raise e end; end
22 class IssuesController; def rescue_action(e) raise e end; end
23
23
24 class IssuesControllerTest < Test::Unit::TestCase
24 class IssuesControllerTest < Test::Unit::TestCase
25 fixtures :projects,
25 fixtures :projects,
26 :users,
26 :users,
27 :roles,
27 :roles,
28 :members,
28 :members,
29 :issues,
29 :issues,
30 :issue_statuses,
30 :issue_statuses,
31 :versions,
31 :versions,
32 :trackers,
32 :trackers,
33 :projects_trackers,
33 :projects_trackers,
34 :issue_categories,
34 :issue_categories,
35 :enabled_modules,
35 :enabled_modules,
36 :enumerations,
36 :enumerations,
37 :attachments,
37 :attachments,
38 :workflows,
38 :workflows,
39 :custom_fields,
39 :custom_fields,
40 :custom_values,
40 :custom_values,
41 :custom_fields_trackers,
41 :custom_fields_trackers,
42 :time_entries,
42 :time_entries,
43 :journals,
43 :journals,
44 :journal_details
44 :journal_details
45
45
46 def setup
46 def setup
47 @controller = IssuesController.new
47 @controller = IssuesController.new
48 @request = ActionController::TestRequest.new
48 @request = ActionController::TestRequest.new
49 @response = ActionController::TestResponse.new
49 @response = ActionController::TestResponse.new
50 User.current = nil
50 User.current = nil
51 end
51 end
52
52
53 def test_index
53 def test_index
54 get :index
54 get :index
55 assert_response :success
55 assert_response :success
56 assert_template 'index.rhtml'
56 assert_template 'index.rhtml'
57 assert_not_nil assigns(:issues)
57 assert_not_nil assigns(:issues)
58 assert_nil assigns(:project)
58 assert_nil assigns(:project)
59 assert_tag :tag => 'a', :content => /Can't print recipes/
59 assert_tag :tag => 'a', :content => /Can't print recipes/
60 assert_tag :tag => 'a', :content => /Subproject issue/
60 assert_tag :tag => 'a', :content => /Subproject issue/
61 # private projects hidden
61 # private projects hidden
62 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
62 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
63 assert_no_tag :tag => 'a', :content => /Issue on project 2/
63 assert_no_tag :tag => 'a', :content => /Issue on project 2/
64 end
64 end
65
65
66 def test_index_should_not_list_issues_when_module_disabled
66 def test_index_should_not_list_issues_when_module_disabled
67 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
67 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
68 get :index
68 get :index
69 assert_response :success
69 assert_response :success
70 assert_template 'index.rhtml'
70 assert_template 'index.rhtml'
71 assert_not_nil assigns(:issues)
71 assert_not_nil assigns(:issues)
72 assert_nil assigns(:project)
72 assert_nil assigns(:project)
73 assert_no_tag :tag => 'a', :content => /Can't print recipes/
73 assert_no_tag :tag => 'a', :content => /Can't print recipes/
74 assert_tag :tag => 'a', :content => /Subproject issue/
74 assert_tag :tag => 'a', :content => /Subproject issue/
75 end
75 end
76
76
77 def test_index_with_project
77 def test_index_with_project
78 Setting.display_subprojects_issues = 0
78 Setting.display_subprojects_issues = 0
79 get :index, :project_id => 1
79 get :index, :project_id => 1
80 assert_response :success
80 assert_response :success
81 assert_template 'index.rhtml'
81 assert_template 'index.rhtml'
82 assert_not_nil assigns(:issues)
82 assert_not_nil assigns(:issues)
83 assert_tag :tag => 'a', :content => /Can't print recipes/
83 assert_tag :tag => 'a', :content => /Can't print recipes/
84 assert_no_tag :tag => 'a', :content => /Subproject issue/
84 assert_no_tag :tag => 'a', :content => /Subproject issue/
85 end
85 end
86
86
87 def test_index_with_project_and_subprojects
87 def test_index_with_project_and_subprojects
88 Setting.display_subprojects_issues = 1
88 Setting.display_subprojects_issues = 1
89 get :index, :project_id => 1
89 get :index, :project_id => 1
90 assert_response :success
90 assert_response :success
91 assert_template 'index.rhtml'
91 assert_template 'index.rhtml'
92 assert_not_nil assigns(:issues)
92 assert_not_nil assigns(:issues)
93 assert_tag :tag => 'a', :content => /Can't print recipes/
93 assert_tag :tag => 'a', :content => /Can't print recipes/
94 assert_tag :tag => 'a', :content => /Subproject issue/
94 assert_tag :tag => 'a', :content => /Subproject issue/
95 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
95 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
96 end
96 end
97
97
98 def test_index_with_project_and_subprojects_should_show_private_subprojects
98 def test_index_with_project_and_subprojects_should_show_private_subprojects
99 @request.session[:user_id] = 2
99 @request.session[:user_id] = 2
100 Setting.display_subprojects_issues = 1
100 Setting.display_subprojects_issues = 1
101 get :index, :project_id => 1
101 get :index, :project_id => 1
102 assert_response :success
102 assert_response :success
103 assert_template 'index.rhtml'
103 assert_template 'index.rhtml'
104 assert_not_nil assigns(:issues)
104 assert_not_nil assigns(:issues)
105 assert_tag :tag => 'a', :content => /Can't print recipes/
105 assert_tag :tag => 'a', :content => /Can't print recipes/
106 assert_tag :tag => 'a', :content => /Subproject issue/
106 assert_tag :tag => 'a', :content => /Subproject issue/
107 assert_tag :tag => 'a', :content => /Issue of a private subproject/
107 assert_tag :tag => 'a', :content => /Issue of a private subproject/
108 end
108 end
109
109
110 def test_index_with_project_and_filter
110 def test_index_with_project_and_filter
111 get :index, :project_id => 1, :set_filter => 1
111 get :index, :project_id => 1, :set_filter => 1
112 assert_response :success
112 assert_response :success
113 assert_template 'index.rhtml'
113 assert_template 'index.rhtml'
114 assert_not_nil assigns(:issues)
114 assert_not_nil assigns(:issues)
115 end
115 end
116
116
117 def test_index_csv_with_project
117 def test_index_csv_with_project
118 get :index, :format => 'csv'
118 get :index, :format => 'csv'
119 assert_response :success
119 assert_response :success
120 assert_not_nil assigns(:issues)
120 assert_not_nil assigns(:issues)
121 assert_equal 'text/csv', @response.content_type
121 assert_equal 'text/csv', @response.content_type
122
122
123 get :index, :project_id => 1, :format => 'csv'
123 get :index, :project_id => 1, :format => 'csv'
124 assert_response :success
124 assert_response :success
125 assert_not_nil assigns(:issues)
125 assert_not_nil assigns(:issues)
126 assert_equal 'text/csv', @response.content_type
126 assert_equal 'text/csv', @response.content_type
127 end
127 end
128
128
129 def test_index_pdf
129 def test_index_pdf
130 get :index, :format => 'pdf'
130 get :index, :format => 'pdf'
131 assert_response :success
131 assert_response :success
132 assert_not_nil assigns(:issues)
132 assert_not_nil assigns(:issues)
133 assert_equal 'application/pdf', @response.content_type
133 assert_equal 'application/pdf', @response.content_type
134
134
135 get :index, :project_id => 1, :format => 'pdf'
135 get :index, :project_id => 1, :format => 'pdf'
136 assert_response :success
136 assert_response :success
137 assert_not_nil assigns(:issues)
137 assert_not_nil assigns(:issues)
138 assert_equal 'application/pdf', @response.content_type
138 assert_equal 'application/pdf', @response.content_type
139 end
139 end
140
140
141 def test_gantt
141 def test_gantt
142 get :gantt, :project_id => 1
142 get :gantt, :project_id => 1
143 assert_response :success
143 assert_response :success
144 assert_template 'gantt.rhtml'
144 assert_template 'gantt.rhtml'
145 assert_not_nil assigns(:gantt)
145 assert_not_nil assigns(:gantt)
146 events = assigns(:gantt).events
146 events = assigns(:gantt).events
147 assert_not_nil events
147 assert_not_nil events
148 # Issue with start and due dates
148 # Issue with start and due dates
149 i = Issue.find(1)
149 i = Issue.find(1)
150 assert_not_nil i.due_date
150 assert_not_nil i.due_date
151 assert events.include?(Issue.find(1))
151 assert events.include?(Issue.find(1))
152 # Issue with without due date but targeted to a version with date
152 # Issue with without due date but targeted to a version with date
153 i = Issue.find(2)
153 i = Issue.find(2)
154 assert_nil i.due_date
154 assert_nil i.due_date
155 assert events.include?(i)
155 assert events.include?(i)
156 end
156 end
157
157
158 def test_cross_project_gantt
158 def test_cross_project_gantt
159 get :gantt
159 get :gantt
160 assert_response :success
160 assert_response :success
161 assert_template 'gantt.rhtml'
161 assert_template 'gantt.rhtml'
162 assert_not_nil assigns(:gantt)
162 assert_not_nil assigns(:gantt)
163 events = assigns(:gantt).events
163 events = assigns(:gantt).events
164 assert_not_nil events
164 assert_not_nil events
165 end
165 end
166
166
167 def test_gantt_export_to_pdf
167 def test_gantt_export_to_pdf
168 get :gantt, :project_id => 1, :format => 'pdf'
168 get :gantt, :project_id => 1, :format => 'pdf'
169 assert_response :success
169 assert_response :success
170 assert_template 'gantt.rfpdf'
170 assert_template 'gantt.rfpdf'
171 assert_equal 'application/pdf', @response.content_type
171 assert_equal 'application/pdf', @response.content_type
172 assert_not_nil assigns(:gantt)
172 assert_not_nil assigns(:gantt)
173 end
173 end
174
174
175 def test_cross_project_gantt_export_to_pdf
175 def test_cross_project_gantt_export_to_pdf
176 get :gantt, :format => 'pdf'
176 get :gantt, :format => 'pdf'
177 assert_response :success
177 assert_response :success
178 assert_template 'gantt.rfpdf'
178 assert_template 'gantt.rfpdf'
179 assert_equal 'application/pdf', @response.content_type
179 assert_equal 'application/pdf', @response.content_type
180 assert_not_nil assigns(:gantt)
180 assert_not_nil assigns(:gantt)
181 end
181 end
182
182
183 if Object.const_defined?(:Magick)
183 if Object.const_defined?(:Magick)
184 def test_gantt_image
184 def test_gantt_image
185 get :gantt, :project_id => 1, :format => 'png'
185 get :gantt, :project_id => 1, :format => 'png'
186 assert_response :success
186 assert_response :success
187 assert_equal 'image/png', @response.content_type
187 assert_equal 'image/png', @response.content_type
188 end
188 end
189 else
189 else
190 puts "RMagick not installed. Skipping tests !!!"
190 puts "RMagick not installed. Skipping tests !!!"
191 end
191 end
192
192
193 def test_calendar
193 def test_calendar
194 get :calendar, :project_id => 1
194 get :calendar, :project_id => 1
195 assert_response :success
195 assert_response :success
196 assert_template 'calendar'
196 assert_template 'calendar'
197 assert_not_nil assigns(:calendar)
197 assert_not_nil assigns(:calendar)
198 end
198 end
199
199
200 def test_cross_project_calendar
200 def test_cross_project_calendar
201 get :calendar
201 get :calendar
202 assert_response :success
202 assert_response :success
203 assert_template 'calendar'
203 assert_template 'calendar'
204 assert_not_nil assigns(:calendar)
204 assert_not_nil assigns(:calendar)
205 end
205 end
206
206
207 def test_changes
207 def test_changes
208 get :changes, :project_id => 1
208 get :changes, :project_id => 1
209 assert_response :success
209 assert_response :success
210 assert_not_nil assigns(:journals)
210 assert_not_nil assigns(:journals)
211 assert_equal 'application/atom+xml', @response.content_type
211 assert_equal 'application/atom+xml', @response.content_type
212 end
212 end
213
213
214 def test_show_by_anonymous
214 def test_show_by_anonymous
215 get :show, :id => 1
215 get :show, :id => 1
216 assert_response :success
216 assert_response :success
217 assert_template 'show.rhtml'
217 assert_template 'show.rhtml'
218 assert_not_nil assigns(:issue)
218 assert_not_nil assigns(:issue)
219 assert_equal Issue.find(1), assigns(:issue)
219 assert_equal Issue.find(1), assigns(:issue)
220
220
221 # anonymous role is allowed to add a note
221 # anonymous role is allowed to add a note
222 assert_tag :tag => 'form',
222 assert_tag :tag => 'form',
223 :descendant => { :tag => 'fieldset',
223 :descendant => { :tag => 'fieldset',
224 :child => { :tag => 'legend',
224 :child => { :tag => 'legend',
225 :content => /Notes/ } }
225 :content => /Notes/ } }
226 end
226 end
227
227
228 def test_show_by_manager
228 def test_show_by_manager
229 @request.session[:user_id] = 2
229 @request.session[:user_id] = 2
230 get :show, :id => 1
230 get :show, :id => 1
231 assert_response :success
231 assert_response :success
232
232
233 assert_tag :tag => 'form',
233 assert_tag :tag => 'form',
234 :descendant => { :tag => 'fieldset',
234 :descendant => { :tag => 'fieldset',
235 :child => { :tag => 'legend',
235 :child => { :tag => 'legend',
236 :content => /Change properties/ } },
236 :content => /Change properties/ } },
237 :descendant => { :tag => 'fieldset',
237 :descendant => { :tag => 'fieldset',
238 :child => { :tag => 'legend',
238 :child => { :tag => 'legend',
239 :content => /Log time/ } },
239 :content => /Log time/ } },
240 :descendant => { :tag => 'fieldset',
240 :descendant => { :tag => 'fieldset',
241 :child => { :tag => 'legend',
241 :child => { :tag => 'legend',
242 :content => /Notes/ } }
242 :content => /Notes/ } }
243 end
243 end
244
244
245 def test_get_new
245 def test_get_new
246 @request.session[:user_id] = 2
246 @request.session[:user_id] = 2
247 get :new, :project_id => 1, :tracker_id => 1
247 get :new, :project_id => 1, :tracker_id => 1
248 assert_response :success
248 assert_response :success
249 assert_template 'new'
249 assert_template 'new'
250
250
251 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
251 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
252 :value => 'Default string' }
252 :value => 'Default string' }
253 end
253 end
254
254
255 def test_get_new_without_tracker_id
255 def test_get_new_without_tracker_id
256 @request.session[:user_id] = 2
256 @request.session[:user_id] = 2
257 get :new, :project_id => 1
257 get :new, :project_id => 1
258 assert_response :success
258 assert_response :success
259 assert_template 'new'
259 assert_template 'new'
260
260
261 issue = assigns(:issue)
261 issue = assigns(:issue)
262 assert_not_nil issue
262 assert_not_nil issue
263 assert_equal Project.find(1).trackers.first, issue.tracker
263 assert_equal Project.find(1).trackers.first, issue.tracker
264 end
264 end
265
265
266 def test_update_new_form
266 def test_update_new_form
267 @request.session[:user_id] = 2
267 @request.session[:user_id] = 2
268 xhr :post, :new, :project_id => 1,
268 xhr :post, :new, :project_id => 1,
269 :issue => {:tracker_id => 2,
269 :issue => {:tracker_id => 2,
270 :subject => 'This is the test_new issue',
270 :subject => 'This is the test_new issue',
271 :description => 'This is the description',
271 :description => 'This is the description',
272 :priority_id => 5}
272 :priority_id => 5}
273 assert_response :success
273 assert_response :success
274 assert_template 'new'
274 assert_template 'new'
275 end
275 end
276
276
277 def test_post_new
277 def test_post_new
278 @request.session[:user_id] = 2
278 @request.session[:user_id] = 2
279 post :new, :project_id => 1,
279 post :new, :project_id => 1,
280 :issue => {:tracker_id => 3,
280 :issue => {:tracker_id => 3,
281 :subject => 'This is the test_new issue',
281 :subject => 'This is the test_new issue',
282 :description => 'This is the description',
282 :description => 'This is the description',
283 :priority_id => 5,
283 :priority_id => 5,
284 :estimated_hours => '',
284 :estimated_hours => '',
285 :custom_field_values => {'2' => 'Value for field 2'}}
285 :custom_field_values => {'2' => 'Value for field 2'}}
286 assert_redirected_to 'issues/show'
286 assert_redirected_to 'issues/show'
287
287
288 issue = Issue.find_by_subject('This is the test_new issue')
288 issue = Issue.find_by_subject('This is the test_new issue')
289 assert_not_nil issue
289 assert_not_nil issue
290 assert_equal 2, issue.author_id
290 assert_equal 2, issue.author_id
291 assert_equal 3, issue.tracker_id
291 assert_equal 3, issue.tracker_id
292 assert_nil issue.estimated_hours
292 assert_nil issue.estimated_hours
293 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
293 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
294 assert_not_nil v
294 assert_not_nil v
295 assert_equal 'Value for field 2', v.value
295 assert_equal 'Value for field 2', v.value
296 end
296 end
297
297
298 def test_post_new_without_custom_fields_param
298 def test_post_new_without_custom_fields_param
299 @request.session[:user_id] = 2
299 @request.session[:user_id] = 2
300 post :new, :project_id => 1,
300 post :new, :project_id => 1,
301 :issue => {:tracker_id => 1,
301 :issue => {:tracker_id => 1,
302 :subject => 'This is the test_new issue',
302 :subject => 'This is the test_new issue',
303 :description => 'This is the description',
303 :description => 'This is the description',
304 :priority_id => 5}
304 :priority_id => 5}
305 assert_redirected_to 'issues/show'
305 assert_redirected_to 'issues/show'
306 end
306 end
307
307
308 def test_post_new_with_required_custom_field_and_without_custom_fields_param
308 def test_post_new_with_required_custom_field_and_without_custom_fields_param
309 field = IssueCustomField.find_by_name('Database')
309 field = IssueCustomField.find_by_name('Database')
310 field.update_attribute(:is_required, true)
310 field.update_attribute(:is_required, true)
311
311
312 @request.session[:user_id] = 2
312 @request.session[:user_id] = 2
313 post :new, :project_id => 1,
313 post :new, :project_id => 1,
314 :issue => {:tracker_id => 1,
314 :issue => {:tracker_id => 1,
315 :subject => 'This is the test_new issue',
315 :subject => 'This is the test_new issue',
316 :description => 'This is the description',
316 :description => 'This is the description',
317 :priority_id => 5}
317 :priority_id => 5}
318 assert_response :success
318 assert_response :success
319 assert_template 'new'
319 assert_template 'new'
320 issue = assigns(:issue)
320 issue = assigns(:issue)
321 assert_not_nil issue
321 assert_not_nil issue
322 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
322 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
323 end
323 end
324
324
325 def test_post_should_preserve_fields_values_on_validation_failure
325 def test_post_should_preserve_fields_values_on_validation_failure
326 @request.session[:user_id] = 2
326 @request.session[:user_id] = 2
327 post :new, :project_id => 1,
327 post :new, :project_id => 1,
328 :issue => {:tracker_id => 1,
328 :issue => {:tracker_id => 1,
329 :subject => 'This is the test_new issue',
329 :subject => 'This is the test_new issue',
330 # empty description
330 # empty description
331 :description => '',
331 :description => '',
332 :priority_id => 6,
332 :priority_id => 6,
333 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
333 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
334 assert_response :success
334 assert_response :success
335 assert_template 'new'
335 assert_template 'new'
336
336
337 assert_tag :input, :attributes => { :name => 'issue[subject]',
337 assert_tag :input, :attributes => { :name => 'issue[subject]',
338 :value => 'This is the test_new issue' }
338 :value => 'This is the test_new issue' }
339 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
339 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
340 :child => { :tag => 'option', :attributes => { :selected => 'selected',
340 :child => { :tag => 'option', :attributes => { :selected => 'selected',
341 :value => '6' },
341 :value => '6' },
342 :content => 'High' }
342 :content => 'High' }
343 # Custom fields
343 # Custom fields
344 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
344 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
345 :child => { :tag => 'option', :attributes => { :selected => 'selected',
345 :child => { :tag => 'option', :attributes => { :selected => 'selected',
346 :value => 'Oracle' },
346 :value => 'Oracle' },
347 :content => 'Oracle' }
347 :content => 'Oracle' }
348 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
348 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
349 :value => 'Value for field 2'}
349 :value => 'Value for field 2'}
350 end
350 end
351
351
352 def test_copy_issue
352 def test_copy_issue
353 @request.session[:user_id] = 2
353 @request.session[:user_id] = 2
354 get :new, :project_id => 1, :copy_from => 1
354 get :new, :project_id => 1, :copy_from => 1
355 assert_template 'new'
355 assert_template 'new'
356 assert_not_nil assigns(:issue)
356 assert_not_nil assigns(:issue)
357 orig = Issue.find(1)
357 orig = Issue.find(1)
358 assert_equal orig.subject, assigns(:issue).subject
358 assert_equal orig.subject, assigns(:issue).subject
359 end
359 end
360
360
361 def test_get_edit
361 def test_get_edit
362 @request.session[:user_id] = 2
362 @request.session[:user_id] = 2
363 get :edit, :id => 1
363 get :edit, :id => 1
364 assert_response :success
364 assert_response :success
365 assert_template 'edit'
365 assert_template 'edit'
366 assert_not_nil assigns(:issue)
366 assert_not_nil assigns(:issue)
367 assert_equal Issue.find(1), assigns(:issue)
367 assert_equal Issue.find(1), assigns(:issue)
368 end
368 end
369
369
370 def test_get_edit_with_params
370 def test_get_edit_with_params
371 @request.session[:user_id] = 2
371 @request.session[:user_id] = 2
372 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
372 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
373 assert_response :success
373 assert_response :success
374 assert_template 'edit'
374 assert_template 'edit'
375
375
376 issue = assigns(:issue)
376 issue = assigns(:issue)
377 assert_not_nil issue
377 assert_not_nil issue
378
378
379 assert_equal 5, issue.status_id
379 assert_equal 5, issue.status_id
380 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
380 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
381 :child => { :tag => 'option',
381 :child => { :tag => 'option',
382 :content => 'Closed',
382 :content => 'Closed',
383 :attributes => { :selected => 'selected' } }
383 :attributes => { :selected => 'selected' } }
384
384
385 assert_equal 7, issue.priority_id
385 assert_equal 7, issue.priority_id
386 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
386 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
387 :child => { :tag => 'option',
387 :child => { :tag => 'option',
388 :content => 'Urgent',
388 :content => 'Urgent',
389 :attributes => { :selected => 'selected' } }
389 :attributes => { :selected => 'selected' } }
390 end
390 end
391
391
392 def test_reply_to_issue
392 def test_reply_to_issue
393 @request.session[:user_id] = 2
393 @request.session[:user_id] = 2
394 get :reply, :id => 1
394 get :reply, :id => 1
395 assert_response :success
395 assert_response :success
396 assert_select_rjs :show, "update"
396 assert_select_rjs :show, "update"
397 end
397 end
398
398
399 def test_reply_to_note
399 def test_reply_to_note
400 @request.session[:user_id] = 2
400 @request.session[:user_id] = 2
401 get :reply, :id => 1, :journal_id => 2
401 get :reply, :id => 1, :journal_id => 2
402 assert_response :success
402 assert_response :success
403 assert_select_rjs :show, "update"
403 assert_select_rjs :show, "update"
404 end
404 end
405
405
406 def test_post_edit_without_custom_fields_param
406 def test_post_edit_without_custom_fields_param
407 @request.session[:user_id] = 2
407 @request.session[:user_id] = 2
408 ActionMailer::Base.deliveries.clear
408 ActionMailer::Base.deliveries.clear
409
409
410 issue = Issue.find(1)
410 issue = Issue.find(1)
411 assert_equal '125', issue.custom_value_for(2).value
411 assert_equal '125', issue.custom_value_for(2).value
412 old_subject = issue.subject
412 old_subject = issue.subject
413 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
413 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
414
414
415 assert_difference('Journal.count') do
415 assert_difference('Journal.count') do
416 assert_difference('JournalDetail.count', 2) do
416 assert_difference('JournalDetail.count', 2) do
417 post :edit, :id => 1, :issue => {:subject => new_subject,
417 post :edit, :id => 1, :issue => {:subject => new_subject,
418 :priority_id => '6',
418 :priority_id => '6',
419 :category_id => '1' # no change
419 :category_id => '1' # no change
420 }
420 }
421 end
421 end
422 end
422 end
423 assert_redirected_to 'issues/show/1'
423 assert_redirected_to 'issues/show/1'
424 issue.reload
424 issue.reload
425 assert_equal new_subject, issue.subject
425 assert_equal new_subject, issue.subject
426 # Make sure custom fields were not cleared
426 # Make sure custom fields were not cleared
427 assert_equal '125', issue.custom_value_for(2).value
427 assert_equal '125', issue.custom_value_for(2).value
428
428
429 mail = ActionMailer::Base.deliveries.last
429 mail = ActionMailer::Base.deliveries.last
430 assert_kind_of TMail::Mail, mail
430 assert_kind_of TMail::Mail, mail
431 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
431 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
432 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
432 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
433 end
433 end
434
434
435 def test_post_edit_with_custom_field_change
435 def test_post_edit_with_custom_field_change
436 @request.session[:user_id] = 2
436 @request.session[:user_id] = 2
437 issue = Issue.find(1)
437 issue = Issue.find(1)
438 assert_equal '125', issue.custom_value_for(2).value
438 assert_equal '125', issue.custom_value_for(2).value
439
439
440 assert_difference('Journal.count') do
440 assert_difference('Journal.count') do
441 assert_difference('JournalDetail.count', 3) do
441 assert_difference('JournalDetail.count', 3) do
442 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
442 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
443 :priority_id => '6',
443 :priority_id => '6',
444 :category_id => '1', # no change
444 :category_id => '1', # no change
445 :custom_field_values => { '2' => 'New custom value' }
445 :custom_field_values => { '2' => 'New custom value' }
446 }
446 }
447 end
447 end
448 end
448 end
449 assert_redirected_to 'issues/show/1'
449 assert_redirected_to 'issues/show/1'
450 issue.reload
450 issue.reload
451 assert_equal 'New custom value', issue.custom_value_for(2).value
451 assert_equal 'New custom value', issue.custom_value_for(2).value
452
452
453 mail = ActionMailer::Base.deliveries.last
453 mail = ActionMailer::Base.deliveries.last
454 assert_kind_of TMail::Mail, mail
454 assert_kind_of TMail::Mail, mail
455 assert mail.body.include?("Searchable field changed from 125 to New custom value")
455 assert mail.body.include?("Searchable field changed from 125 to New custom value")
456 end
456 end
457
457
458 def test_post_edit_with_status_and_assignee_change
458 def test_post_edit_with_status_and_assignee_change
459 issue = Issue.find(1)
459 issue = Issue.find(1)
460 assert_equal 1, issue.status_id
460 assert_equal 1, issue.status_id
461 @request.session[:user_id] = 2
461 @request.session[:user_id] = 2
462 assert_difference('TimeEntry.count', 0) do
462 assert_difference('TimeEntry.count', 0) do
463 post :edit,
463 post :edit,
464 :id => 1,
464 :id => 1,
465 :issue => { :status_id => 2, :assigned_to_id => 3 },
465 :issue => { :status_id => 2, :assigned_to_id => 3 },
466 :notes => 'Assigned to dlopper',
466 :notes => 'Assigned to dlopper',
467 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
467 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
468 end
468 end
469 assert_redirected_to 'issues/show/1'
469 assert_redirected_to 'issues/show/1'
470 issue.reload
470 issue.reload
471 assert_equal 2, issue.status_id
471 assert_equal 2, issue.status_id
472 j = issue.journals.find(:first, :order => 'id DESC')
472 j = issue.journals.find(:first, :order => 'id DESC')
473 assert_equal 'Assigned to dlopper', j.notes
473 assert_equal 'Assigned to dlopper', j.notes
474 assert_equal 2, j.details.size
474 assert_equal 2, j.details.size
475
475
476 mail = ActionMailer::Base.deliveries.last
476 mail = ActionMailer::Base.deliveries.last
477 assert mail.body.include?("Status changed from New to Assigned")
477 assert mail.body.include?("Status changed from New to Assigned")
478 end
478 end
479
479
480 def test_post_edit_with_note_only
480 def test_post_edit_with_note_only
481 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
481 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
482 # anonymous user
482 # anonymous user
483 post :edit,
483 post :edit,
484 :id => 1,
484 :id => 1,
485 :notes => notes
485 :notes => notes
486 assert_redirected_to 'issues/show/1'
486 assert_redirected_to 'issues/show/1'
487 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
487 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
488 assert_equal notes, j.notes
488 assert_equal notes, j.notes
489 assert_equal 0, j.details.size
489 assert_equal 0, j.details.size
490 assert_equal User.anonymous, j.user
490 assert_equal User.anonymous, j.user
491
491
492 mail = ActionMailer::Base.deliveries.last
492 mail = ActionMailer::Base.deliveries.last
493 assert mail.body.include?(notes)
493 assert mail.body.include?(notes)
494 end
494 end
495
495
496 def test_post_edit_with_note_and_spent_time
496 def test_post_edit_with_note_and_spent_time
497 @request.session[:user_id] = 2
497 @request.session[:user_id] = 2
498 spent_hours_before = Issue.find(1).spent_hours
498 spent_hours_before = Issue.find(1).spent_hours
499 assert_difference('TimeEntry.count') do
499 assert_difference('TimeEntry.count') do
500 post :edit,
500 post :edit,
501 :id => 1,
501 :id => 1,
502 :notes => '2.5 hours added',
502 :notes => '2.5 hours added',
503 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
503 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
504 end
504 end
505 assert_redirected_to 'issues/show/1'
505 assert_redirected_to 'issues/show/1'
506
506
507 issue = Issue.find(1)
507 issue = Issue.find(1)
508
508
509 j = issue.journals.find(:first, :order => 'id DESC')
509 j = issue.journals.find(:first, :order => 'id DESC')
510 assert_equal '2.5 hours added', j.notes
510 assert_equal '2.5 hours added', j.notes
511 assert_equal 0, j.details.size
511 assert_equal 0, j.details.size
512
512
513 t = issue.time_entries.find(:first, :order => 'id DESC')
513 t = issue.time_entries.find(:first, :order => 'id DESC')
514 assert_not_nil t
514 assert_not_nil t
515 assert_equal 2.5, t.hours
515 assert_equal 2.5, t.hours
516 assert_equal spent_hours_before + 2.5, issue.spent_hours
516 assert_equal spent_hours_before + 2.5, issue.spent_hours
517 end
517 end
518
518
519 def test_post_edit_with_attachment_only
519 def test_post_edit_with_attachment_only
520 set_tmp_attachments_directory
520 set_tmp_attachments_directory
521
521
522 # Delete all fixtured journals, a race condition can occur causing the wrong
522 # Delete all fixtured journals, a race condition can occur causing the wrong
523 # journal to get fetched in the next find.
523 # journal to get fetched in the next find.
524 Journal.delete_all
524 Journal.delete_all
525
525
526 # anonymous user
526 # anonymous user
527 post :edit,
527 post :edit,
528 :id => 1,
528 :id => 1,
529 :notes => '',
529 :notes => '',
530 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
530 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
531 assert_redirected_to 'issues/show/1'
531 assert_redirected_to 'issues/show/1'
532 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
532 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
533 assert j.notes.blank?
533 assert j.notes.blank?
534 assert_equal 1, j.details.size
534 assert_equal 1, j.details.size
535 assert_equal 'testfile.txt', j.details.first.value
535 assert_equal 'testfile.txt', j.details.first.value
536 assert_equal User.anonymous, j.user
536 assert_equal User.anonymous, j.user
537
537
538 mail = ActionMailer::Base.deliveries.last
538 mail = ActionMailer::Base.deliveries.last
539 assert mail.body.include?('testfile.txt')
539 assert mail.body.include?('testfile.txt')
540 end
540 end
541
541
542 def test_post_edit_with_no_change
542 def test_post_edit_with_no_change
543 issue = Issue.find(1)
543 issue = Issue.find(1)
544 issue.journals.clear
544 issue.journals.clear
545 ActionMailer::Base.deliveries.clear
545 ActionMailer::Base.deliveries.clear
546
546
547 post :edit,
547 post :edit,
548 :id => 1,
548 :id => 1,
549 :notes => ''
549 :notes => ''
550 assert_redirected_to 'issues/show/1'
550 assert_redirected_to 'issues/show/1'
551
551
552 issue.reload
552 issue.reload
553 assert issue.journals.empty?
553 assert issue.journals.empty?
554 # No email should be sent
554 # No email should be sent
555 assert ActionMailer::Base.deliveries.empty?
555 assert ActionMailer::Base.deliveries.empty?
556 end
556 end
557
557
558 def test_bulk_edit
558 def test_bulk_edit
559 @request.session[:user_id] = 2
559 @request.session[:user_id] = 2
560 # update issues priority
560 # update issues priority
561 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
561 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
562 assert_response 302
562 assert_response 302
563 # check that the issues were updated
563 # check that the issues were updated
564 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
564 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
565 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
565 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
566 end
566 end
567
567
568 def test_bulk_unassign
568 def test_bulk_unassign
569 assert_not_nil Issue.find(2).assigned_to
569 assert_not_nil Issue.find(2).assigned_to
570 @request.session[:user_id] = 2
570 @request.session[:user_id] = 2
571 # unassign issues
571 # unassign issues
572 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
572 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
573 assert_response 302
573 assert_response 302
574 # check that the issues were updated
574 # check that the issues were updated
575 assert_nil Issue.find(2).assigned_to
575 assert_nil Issue.find(2).assigned_to
576 end
576 end
577
577
578 def test_move_one_issue_to_another_project
578 def test_move_one_issue_to_another_project
579 @request.session[:user_id] = 1
579 @request.session[:user_id] = 1
580 post :move, :id => 1, :new_project_id => 2
580 post :move, :id => 1, :new_project_id => 2
581 assert_redirected_to 'projects/ecookbook/issues'
581 assert_redirected_to 'projects/ecookbook/issues'
582 assert_equal 2, Issue.find(1).project_id
582 assert_equal 2, Issue.find(1).project_id
583 end
583 end
584
584
585 def test_bulk_move_to_another_project
585 def test_bulk_move_to_another_project
586 @request.session[:user_id] = 1
586 @request.session[:user_id] = 1
587 post :move, :ids => [1, 2], :new_project_id => 2
587 post :move, :ids => [1, 2], :new_project_id => 2
588 assert_redirected_to 'projects/ecookbook/issues'
588 assert_redirected_to 'projects/ecookbook/issues'
589 # Issues moved to project 2
589 # Issues moved to project 2
590 assert_equal 2, Issue.find(1).project_id
590 assert_equal 2, Issue.find(1).project_id
591 assert_equal 2, Issue.find(2).project_id
591 assert_equal 2, Issue.find(2).project_id
592 # No tracker change
592 # No tracker change
593 assert_equal 1, Issue.find(1).tracker_id
593 assert_equal 1, Issue.find(1).tracker_id
594 assert_equal 2, Issue.find(2).tracker_id
594 assert_equal 2, Issue.find(2).tracker_id
595 end
595 end
596
596
597 def test_bulk_move_to_another_tracker
597 def test_bulk_move_to_another_tracker
598 @request.session[:user_id] = 1
598 @request.session[:user_id] = 1
599 post :move, :ids => [1, 2], :new_tracker_id => 2
599 post :move, :ids => [1, 2], :new_tracker_id => 2
600 assert_redirected_to 'projects/ecookbook/issues'
600 assert_redirected_to 'projects/ecookbook/issues'
601 assert_equal 2, Issue.find(1).tracker_id
601 assert_equal 2, Issue.find(1).tracker_id
602 assert_equal 2, Issue.find(2).tracker_id
602 assert_equal 2, Issue.find(2).tracker_id
603 end
603 end
604
604
605 def test_context_menu_one_issue
605 def test_context_menu_one_issue
606 @request.session[:user_id] = 2
606 @request.session[:user_id] = 2
607 get :context_menu, :ids => [1]
607 get :context_menu, :ids => [1]
608 assert_response :success
608 assert_response :success
609 assert_template 'context_menu'
609 assert_template 'context_menu'
610 assert_tag :tag => 'a', :content => 'Edit',
610 assert_tag :tag => 'a', :content => 'Edit',
611 :attributes => { :href => '/issues/edit/1',
611 :attributes => { :href => '/issues/edit/1',
612 :class => 'icon-edit' }
612 :class => 'icon-edit' }
613 assert_tag :tag => 'a', :content => 'Closed',
613 assert_tag :tag => 'a', :content => 'Closed',
614 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
614 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
615 :class => '' }
615 :class => '' }
616 assert_tag :tag => 'a', :content => 'Immediate',
616 assert_tag :tag => 'a', :content => 'Immediate',
617 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
617 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
618 :class => '' }
618 :class => '' }
619 assert_tag :tag => 'a', :content => 'Dave Lopper',
619 assert_tag :tag => 'a', :content => 'Dave Lopper',
620 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
620 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
621 :class => '' }
621 :class => '' }
622 assert_tag :tag => 'a', :content => 'Copy',
622 assert_tag :tag => 'a', :content => 'Copy',
623 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
623 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
624 :class => 'icon-copy' }
624 :class => 'icon-copy' }
625 assert_tag :tag => 'a', :content => 'Move',
625 assert_tag :tag => 'a', :content => 'Move',
626 :attributes => { :href => '/issues/move?ids%5B%5D=1',
626 :attributes => { :href => '/issues/move?ids%5B%5D=1',
627 :class => 'icon-move' }
627 :class => 'icon-move' }
628 assert_tag :tag => 'a', :content => 'Delete',
628 assert_tag :tag => 'a', :content => 'Delete',
629 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
629 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
630 :class => 'icon-del' }
630 :class => 'icon-del' }
631 end
631 end
632
632
633 def test_context_menu_one_issue_by_anonymous
633 def test_context_menu_one_issue_by_anonymous
634 get :context_menu, :ids => [1]
634 get :context_menu, :ids => [1]
635 assert_response :success
635 assert_response :success
636 assert_template 'context_menu'
636 assert_template 'context_menu'
637 assert_tag :tag => 'a', :content => 'Delete',
637 assert_tag :tag => 'a', :content => 'Delete',
638 :attributes => { :href => '#',
638 :attributes => { :href => '#',
639 :class => 'icon-del disabled' }
639 :class => 'icon-del disabled' }
640 end
640 end
641
641
642 def test_context_menu_multiple_issues_of_same_project
642 def test_context_menu_multiple_issues_of_same_project
643 @request.session[:user_id] = 2
643 @request.session[:user_id] = 2
644 get :context_menu, :ids => [1, 2]
644 get :context_menu, :ids => [1, 2]
645 assert_response :success
645 assert_response :success
646 assert_template 'context_menu'
646 assert_template 'context_menu'
647 assert_tag :tag => 'a', :content => 'Edit',
647 assert_tag :tag => 'a', :content => 'Edit',
648 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
648 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
649 :class => 'icon-edit' }
649 :class => 'icon-edit' }
650 assert_tag :tag => 'a', :content => 'Immediate',
650 assert_tag :tag => 'a', :content => 'Immediate',
651 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
651 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
652 :class => '' }
652 :class => '' }
653 assert_tag :tag => 'a', :content => 'Dave Lopper',
653 assert_tag :tag => 'a', :content => 'Dave Lopper',
654 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
654 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
655 :class => '' }
655 :class => '' }
656 assert_tag :tag => 'a', :content => 'Move',
656 assert_tag :tag => 'a', :content => 'Move',
657 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
657 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
658 :class => 'icon-move' }
658 :class => 'icon-move' }
659 assert_tag :tag => 'a', :content => 'Delete',
659 assert_tag :tag => 'a', :content => 'Delete',
660 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
660 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
661 :class => 'icon-del' }
661 :class => 'icon-del' }
662 end
662 end
663
663
664 def test_context_menu_multiple_issues_of_different_project
664 def test_context_menu_multiple_issues_of_different_project
665 @request.session[:user_id] = 2
665 @request.session[:user_id] = 2
666 get :context_menu, :ids => [1, 2, 4]
666 get :context_menu, :ids => [1, 2, 4]
667 assert_response :success
667 assert_response :success
668 assert_template 'context_menu'
668 assert_template 'context_menu'
669 assert_tag :tag => 'a', :content => 'Delete',
669 assert_tag :tag => 'a', :content => 'Delete',
670 :attributes => { :href => '#',
670 :attributes => { :href => '#',
671 :class => 'icon-del disabled' }
671 :class => 'icon-del disabled' }
672 end
672 end
673
673
674 def test_destroy_issue_with_no_time_entries
674 def test_destroy_issue_with_no_time_entries
675 assert_nil TimeEntry.find_by_issue_id(2)
675 assert_nil TimeEntry.find_by_issue_id(2)
676 @request.session[:user_id] = 2
676 @request.session[:user_id] = 2
677 post :destroy, :id => 2
677 post :destroy, :id => 2
678 assert_redirected_to 'projects/ecookbook/issues'
678 assert_redirected_to 'projects/ecookbook/issues'
679 assert_nil Issue.find_by_id(2)
679 assert_nil Issue.find_by_id(2)
680 end
680 end
681
681
682 def test_destroy_issues_with_time_entries
682 def test_destroy_issues_with_time_entries
683 @request.session[:user_id] = 2
683 @request.session[:user_id] = 2
684 post :destroy, :ids => [1, 3]
684 post :destroy, :ids => [1, 3]
685 assert_response :success
685 assert_response :success
686 assert_template 'destroy'
686 assert_template 'destroy'
687 assert_not_nil assigns(:hours)
687 assert_not_nil assigns(:hours)
688 assert Issue.find_by_id(1) && Issue.find_by_id(3)
688 assert Issue.find_by_id(1) && Issue.find_by_id(3)
689 end
689 end
690
690
691 def test_destroy_issues_and_destroy_time_entries
691 def test_destroy_issues_and_destroy_time_entries
692 @request.session[:user_id] = 2
692 @request.session[:user_id] = 2
693 post :destroy, :ids => [1, 3], :todo => 'destroy'
693 post :destroy, :ids => [1, 3], :todo => 'destroy'
694 assert_redirected_to 'projects/ecookbook/issues'
694 assert_redirected_to 'projects/ecookbook/issues'
695 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
695 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
696 assert_nil TimeEntry.find_by_id([1, 2])
696 assert_nil TimeEntry.find_by_id([1, 2])
697 end
697 end
698
698
699 def test_destroy_issues_and_assign_time_entries_to_project
699 def test_destroy_issues_and_assign_time_entries_to_project
700 @request.session[:user_id] = 2
700 @request.session[:user_id] = 2
701 post :destroy, :ids => [1, 3], :todo => 'nullify'
701 post :destroy, :ids => [1, 3], :todo => 'nullify'
702 assert_redirected_to 'projects/ecookbook/issues'
702 assert_redirected_to 'projects/ecookbook/issues'
703 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
703 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
704 assert_nil TimeEntry.find(1).issue_id
704 assert_nil TimeEntry.find(1).issue_id
705 assert_nil TimeEntry.find(2).issue_id
705 assert_nil TimeEntry.find(2).issue_id
706 end
706 end
707
707
708 def test_destroy_issues_and_reassign_time_entries_to_another_issue
708 def test_destroy_issues_and_reassign_time_entries_to_another_issue
709 @request.session[:user_id] = 2
709 @request.session[:user_id] = 2
710 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
710 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
711 assert_redirected_to 'projects/ecookbook/issues'
711 assert_redirected_to 'projects/ecookbook/issues'
712 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
712 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
713 assert_equal 2, TimeEntry.find(1).issue_id
713 assert_equal 2, TimeEntry.find(1).issue_id
714 assert_equal 2, TimeEntry.find(2).issue_id
714 assert_equal 2, TimeEntry.find(2).issue_id
715 end
715 end
716
717 def test_destroy_attachment
718 issue = Issue.find(3)
719 a = issue.attachments.size
720 @request.session[:user_id] = 2
721 post :destroy_attachment, :id => 3, :attachment_id => 1
722 assert_redirected_to 'issues/show/3'
723 assert_nil Attachment.find_by_id(1)
724 issue.reload
725 assert_equal((a-1), issue.attachments.size)
726 j = issue.journals.find(:first, :order => 'created_on DESC')
727 assert_equal 'attachment', j.details.first.property
728 end
729 end
716 end
@@ -1,261 +1,254
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'wiki_controller'
19 require 'wiki_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class WikiController; def rescue_action(e) raise e end; end
22 class WikiController; def rescue_action(e) raise e end; end
23
23
24 class WikiControllerTest < Test::Unit::TestCase
24 class WikiControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :roles, :members, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :attachments
25 fixtures :projects, :users, :roles, :members, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :attachments
26
26
27 def setup
27 def setup
28 @controller = WikiController.new
28 @controller = WikiController.new
29 @request = ActionController::TestRequest.new
29 @request = ActionController::TestRequest.new
30 @response = ActionController::TestResponse.new
30 @response = ActionController::TestResponse.new
31 User.current = nil
31 User.current = nil
32 end
32 end
33
33
34 def test_show_start_page
34 def test_show_start_page
35 get :index, :id => 'ecookbook'
35 get :index, :id => 'ecookbook'
36 assert_response :success
36 assert_response :success
37 assert_template 'show'
37 assert_template 'show'
38 assert_tag :tag => 'h1', :content => /CookBook documentation/
38 assert_tag :tag => 'h1', :content => /CookBook documentation/
39
39
40 # child_pages macro
40 # child_pages macro
41 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
41 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
42 :child => { :tag => 'li',
42 :child => { :tag => 'li',
43 :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/Page_with_an_inline_image' },
43 :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/Page_with_an_inline_image' },
44 :content => 'Page with an inline image' } }
44 :content => 'Page with an inline image' } }
45 end
45 end
46
46
47 def test_show_page_with_name
47 def test_show_page_with_name
48 get :index, :id => 1, :page => 'Another_page'
48 get :index, :id => 1, :page => 'Another_page'
49 assert_response :success
49 assert_response :success
50 assert_template 'show'
50 assert_template 'show'
51 assert_tag :tag => 'h1', :content => /Another page/
51 assert_tag :tag => 'h1', :content => /Another page/
52 # Included page with an inline image
52 # Included page with an inline image
53 assert_tag :tag => 'p', :content => /This is an inline image/
53 assert_tag :tag => 'p', :content => /This is an inline image/
54 assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3',
54 assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3',
55 :alt => 'This is a logo' }
55 :alt => 'This is a logo' }
56 end
56 end
57
57
58 def test_show_unexistent_page_without_edit_right
58 def test_show_unexistent_page_without_edit_right
59 get :index, :id => 1, :page => 'Unexistent page'
59 get :index, :id => 1, :page => 'Unexistent page'
60 assert_response 404
60 assert_response 404
61 end
61 end
62
62
63 def test_show_unexistent_page_with_edit_right
63 def test_show_unexistent_page_with_edit_right
64 @request.session[:user_id] = 2
64 @request.session[:user_id] = 2
65 get :index, :id => 1, :page => 'Unexistent page'
65 get :index, :id => 1, :page => 'Unexistent page'
66 assert_response :success
66 assert_response :success
67 assert_template 'edit'
67 assert_template 'edit'
68 end
68 end
69
69
70 def test_create_page
70 def test_create_page
71 @request.session[:user_id] = 2
71 @request.session[:user_id] = 2
72 post :edit, :id => 1,
72 post :edit, :id => 1,
73 :page => 'New page',
73 :page => 'New page',
74 :content => {:comments => 'Created the page',
74 :content => {:comments => 'Created the page',
75 :text => "h1. New page\n\nThis is a new page",
75 :text => "h1. New page\n\nThis is a new page",
76 :version => 0}
76 :version => 0}
77 assert_redirected_to 'wiki/ecookbook/New_page'
77 assert_redirected_to 'wiki/ecookbook/New_page'
78 page = Project.find(1).wiki.find_page('New page')
78 page = Project.find(1).wiki.find_page('New page')
79 assert !page.new_record?
79 assert !page.new_record?
80 assert_not_nil page.content
80 assert_not_nil page.content
81 assert_equal 'Created the page', page.content.comments
81 assert_equal 'Created the page', page.content.comments
82 end
82 end
83
83
84 def test_preview
84 def test_preview
85 @request.session[:user_id] = 2
85 @request.session[:user_id] = 2
86 xhr :post, :preview, :id => 1, :page => 'CookBook_documentation',
86 xhr :post, :preview, :id => 1, :page => 'CookBook_documentation',
87 :content => { :comments => '',
87 :content => { :comments => '',
88 :text => 'this is a *previewed text*',
88 :text => 'this is a *previewed text*',
89 :version => 3 }
89 :version => 3 }
90 assert_response :success
90 assert_response :success
91 assert_template 'common/_preview'
91 assert_template 'common/_preview'
92 assert_tag :tag => 'strong', :content => /previewed text/
92 assert_tag :tag => 'strong', :content => /previewed text/
93 end
93 end
94
94
95 def test_preview_new_page
95 def test_preview_new_page
96 @request.session[:user_id] = 2
96 @request.session[:user_id] = 2
97 xhr :post, :preview, :id => 1, :page => 'New page',
97 xhr :post, :preview, :id => 1, :page => 'New page',
98 :content => { :text => 'h1. New page',
98 :content => { :text => 'h1. New page',
99 :comments => '',
99 :comments => '',
100 :version => 0 }
100 :version => 0 }
101 assert_response :success
101 assert_response :success
102 assert_template 'common/_preview'
102 assert_template 'common/_preview'
103 assert_tag :tag => 'h1', :content => /New page/
103 assert_tag :tag => 'h1', :content => /New page/
104 end
104 end
105
105
106 def test_history
106 def test_history
107 get :history, :id => 1, :page => 'CookBook_documentation'
107 get :history, :id => 1, :page => 'CookBook_documentation'
108 assert_response :success
108 assert_response :success
109 assert_template 'history'
109 assert_template 'history'
110 assert_not_nil assigns(:versions)
110 assert_not_nil assigns(:versions)
111 assert_equal 3, assigns(:versions).size
111 assert_equal 3, assigns(:versions).size
112 assert_select "input[type=submit][name=commit]"
112 assert_select "input[type=submit][name=commit]"
113 end
113 end
114
114
115 def test_history_with_one_version
115 def test_history_with_one_version
116 get :history, :id => 1, :page => 'Another_page'
116 get :history, :id => 1, :page => 'Another_page'
117 assert_response :success
117 assert_response :success
118 assert_template 'history'
118 assert_template 'history'
119 assert_not_nil assigns(:versions)
119 assert_not_nil assigns(:versions)
120 assert_equal 1, assigns(:versions).size
120 assert_equal 1, assigns(:versions).size
121 assert_select "input[type=submit][name=commit]", false
121 assert_select "input[type=submit][name=commit]", false
122 end
122 end
123
123
124 def test_diff
124 def test_diff
125 get :diff, :id => 1, :page => 'CookBook_documentation', :version => 2, :version_from => 1
125 get :diff, :id => 1, :page => 'CookBook_documentation', :version => 2, :version_from => 1
126 assert_response :success
126 assert_response :success
127 assert_template 'diff'
127 assert_template 'diff'
128 assert_tag :tag => 'span', :attributes => { :class => 'diff_in'},
128 assert_tag :tag => 'span', :attributes => { :class => 'diff_in'},
129 :content => /updated/
129 :content => /updated/
130 end
130 end
131
131
132 def test_annotate
132 def test_annotate
133 get :annotate, :id => 1, :page => 'CookBook_documentation', :version => 2
133 get :annotate, :id => 1, :page => 'CookBook_documentation', :version => 2
134 assert_response :success
134 assert_response :success
135 assert_template 'annotate'
135 assert_template 'annotate'
136 # Line 1
136 # Line 1
137 assert_tag :tag => 'tr', :child => { :tag => 'th', :attributes => {:class => 'line-num'}, :content => '1' },
137 assert_tag :tag => 'tr', :child => { :tag => 'th', :attributes => {:class => 'line-num'}, :content => '1' },
138 :child => { :tag => 'td', :attributes => {:class => 'author'}, :content => /John Smith/ },
138 :child => { :tag => 'td', :attributes => {:class => 'author'}, :content => /John Smith/ },
139 :child => { :tag => 'td', :content => /h1\. CookBook documentation/ }
139 :child => { :tag => 'td', :content => /h1\. CookBook documentation/ }
140 # Line 2
140 # Line 2
141 assert_tag :tag => 'tr', :child => { :tag => 'th', :attributes => {:class => 'line-num'}, :content => '2' },
141 assert_tag :tag => 'tr', :child => { :tag => 'th', :attributes => {:class => 'line-num'}, :content => '2' },
142 :child => { :tag => 'td', :attributes => {:class => 'author'}, :content => /redMine Admin/ },
142 :child => { :tag => 'td', :attributes => {:class => 'author'}, :content => /redMine Admin/ },
143 :child => { :tag => 'td', :content => /Some updated \[\[documentation\]\] here/ }
143 :child => { :tag => 'td', :content => /Some updated \[\[documentation\]\] here/ }
144 end
144 end
145
145
146 def test_rename_with_redirect
146 def test_rename_with_redirect
147 @request.session[:user_id] = 2
147 @request.session[:user_id] = 2
148 post :rename, :id => 1, :page => 'Another_page',
148 post :rename, :id => 1, :page => 'Another_page',
149 :wiki_page => { :title => 'Another renamed page',
149 :wiki_page => { :title => 'Another renamed page',
150 :redirect_existing_links => 1 }
150 :redirect_existing_links => 1 }
151 assert_redirected_to 'wiki/ecookbook/Another_renamed_page'
151 assert_redirected_to 'wiki/ecookbook/Another_renamed_page'
152 wiki = Project.find(1).wiki
152 wiki = Project.find(1).wiki
153 # Check redirects
153 # Check redirects
154 assert_not_nil wiki.find_page('Another page')
154 assert_not_nil wiki.find_page('Another page')
155 assert_nil wiki.find_page('Another page', :with_redirect => false)
155 assert_nil wiki.find_page('Another page', :with_redirect => false)
156 end
156 end
157
157
158 def test_rename_without_redirect
158 def test_rename_without_redirect
159 @request.session[:user_id] = 2
159 @request.session[:user_id] = 2
160 post :rename, :id => 1, :page => 'Another_page',
160 post :rename, :id => 1, :page => 'Another_page',
161 :wiki_page => { :title => 'Another renamed page',
161 :wiki_page => { :title => 'Another renamed page',
162 :redirect_existing_links => "0" }
162 :redirect_existing_links => "0" }
163 assert_redirected_to 'wiki/ecookbook/Another_renamed_page'
163 assert_redirected_to 'wiki/ecookbook/Another_renamed_page'
164 wiki = Project.find(1).wiki
164 wiki = Project.find(1).wiki
165 # Check that there's no redirects
165 # Check that there's no redirects
166 assert_nil wiki.find_page('Another page')
166 assert_nil wiki.find_page('Another page')
167 end
167 end
168
168
169 def test_destroy
169 def test_destroy
170 @request.session[:user_id] = 2
170 @request.session[:user_id] = 2
171 post :destroy, :id => 1, :page => 'CookBook_documentation'
171 post :destroy, :id => 1, :page => 'CookBook_documentation'
172 assert_redirected_to 'wiki/ecookbook/Page_index/special'
172 assert_redirected_to 'wiki/ecookbook/Page_index/special'
173 end
173 end
174
174
175 def test_page_index
175 def test_page_index
176 get :special, :id => 'ecookbook', :page => 'Page_index'
176 get :special, :id => 'ecookbook', :page => 'Page_index'
177 assert_response :success
177 assert_response :success
178 assert_template 'special_page_index'
178 assert_template 'special_page_index'
179 pages = assigns(:pages)
179 pages = assigns(:pages)
180 assert_not_nil pages
180 assert_not_nil pages
181 assert_equal Project.find(1).wiki.pages.size, pages.size
181 assert_equal Project.find(1).wiki.pages.size, pages.size
182
182
183 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
183 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
184 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/CookBook_documentation' },
184 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/CookBook_documentation' },
185 :content => 'CookBook documentation' },
185 :content => 'CookBook documentation' },
186 :child => { :tag => 'ul',
186 :child => { :tag => 'ul',
187 :child => { :tag => 'li',
187 :child => { :tag => 'li',
188 :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/Page_with_an_inline_image' },
188 :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/Page_with_an_inline_image' },
189 :content => 'Page with an inline image' } } } },
189 :content => 'Page with an inline image' } } } },
190 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/Another_page' },
190 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/Another_page' },
191 :content => 'Another page' } }
191 :content => 'Another page' } }
192 end
192 end
193
193
194 def test_not_found
194 def test_not_found
195 get :index, :id => 999
195 get :index, :id => 999
196 assert_response 404
196 assert_response 404
197 end
197 end
198
198
199 def test_protect_page
199 def test_protect_page
200 page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page')
200 page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page')
201 assert !page.protected?
201 assert !page.protected?
202 @request.session[:user_id] = 2
202 @request.session[:user_id] = 2
203 post :protect, :id => 1, :page => page.title, :protected => '1'
203 post :protect, :id => 1, :page => page.title, :protected => '1'
204 assert_redirected_to 'wiki/ecookbook/Another_page'
204 assert_redirected_to 'wiki/ecookbook/Another_page'
205 assert page.reload.protected?
205 assert page.reload.protected?
206 end
206 end
207
207
208 def test_unprotect_page
208 def test_unprotect_page
209 page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation')
209 page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation')
210 assert page.protected?
210 assert page.protected?
211 @request.session[:user_id] = 2
211 @request.session[:user_id] = 2
212 post :protect, :id => 1, :page => page.title, :protected => '0'
212 post :protect, :id => 1, :page => page.title, :protected => '0'
213 assert_redirected_to 'wiki/ecookbook'
213 assert_redirected_to 'wiki/ecookbook'
214 assert !page.reload.protected?
214 assert !page.reload.protected?
215 end
215 end
216
216
217 def test_show_page_with_edit_link
217 def test_show_page_with_edit_link
218 @request.session[:user_id] = 2
218 @request.session[:user_id] = 2
219 get :index, :id => 1
219 get :index, :id => 1
220 assert_response :success
220 assert_response :success
221 assert_template 'show'
221 assert_template 'show'
222 assert_tag :tag => 'a', :attributes => { :href => '/wiki/1/CookBook_documentation/edit' }
222 assert_tag :tag => 'a', :attributes => { :href => '/wiki/1/CookBook_documentation/edit' }
223 end
223 end
224
224
225 def test_show_page_without_edit_link
225 def test_show_page_without_edit_link
226 @request.session[:user_id] = 4
226 @request.session[:user_id] = 4
227 get :index, :id => 1
227 get :index, :id => 1
228 assert_response :success
228 assert_response :success
229 assert_template 'show'
229 assert_template 'show'
230 assert_no_tag :tag => 'a', :attributes => { :href => '/wiki/1/CookBook_documentation/edit' }
230 assert_no_tag :tag => 'a', :attributes => { :href => '/wiki/1/CookBook_documentation/edit' }
231 end
231 end
232
232
233 def test_edit_unprotected_page
233 def test_edit_unprotected_page
234 # Non members can edit unprotected wiki pages
234 # Non members can edit unprotected wiki pages
235 @request.session[:user_id] = 4
235 @request.session[:user_id] = 4
236 get :edit, :id => 1, :page => 'Another_page'
236 get :edit, :id => 1, :page => 'Another_page'
237 assert_response :success
237 assert_response :success
238 assert_template 'edit'
238 assert_template 'edit'
239 end
239 end
240
240
241 def test_edit_protected_page_by_nonmember
241 def test_edit_protected_page_by_nonmember
242 # Non members can't edit protected wiki pages
242 # Non members can't edit protected wiki pages
243 @request.session[:user_id] = 4
243 @request.session[:user_id] = 4
244 get :edit, :id => 1, :page => 'CookBook_documentation'
244 get :edit, :id => 1, :page => 'CookBook_documentation'
245 assert_response 403
245 assert_response 403
246 end
246 end
247
247
248 def test_edit_protected_page_by_member
248 def test_edit_protected_page_by_member
249 @request.session[:user_id] = 2
249 @request.session[:user_id] = 2
250 get :edit, :id => 1, :page => 'CookBook_documentation'
250 get :edit, :id => 1, :page => 'CookBook_documentation'
251 assert_response :success
251 assert_response :success
252 assert_template 'edit'
252 assert_template 'edit'
253 end
253 end
254
255 def test_destroy_attachment
256 @request.session[:user_id] = 2
257 assert_difference 'Attachment.count', -1 do
258 post :destroy_attachment, :id => 1, :page => 'Page_with_an_inline_image', :attachment_id => 3
259 end
260 end
261 end
254 end
General Comments 0
You need to be logged in to leave comments. Login now