##// END OF EJS Templates
Merged r2116, r2117 and r2187 from trunk....
Jean-Philippe Lang -
r2215:31178553f330
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,5 +1,5
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
@@ -17,7 +17,11
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
@@ -37,19 +41,32 class AttachmentsController < ApplicationController
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
@@ -71,11 +71,6 class DocumentsController < ApplicationController
71 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added')
71 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added')
72 redirect_to :action => 'show', :id => @document
72 redirect_to :action => 'show', :id => @document
73 end
73 end
74
75 def destroy_attachment
76 @document.attachments.find(params[:attachment_id]).destroy
77 redirect_to :action => 'show', :id => @document
78 end
79
74
80 private
75 private
81 def find_project
76 def find_project
@@ -18,7 +18,7
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]
@@ -318,17 +318,6 class IssuesController < ApplicationController
318 @issues.each(&:destroy)
318 @issues.each(&:destroy)
319 redirect_to :action => 'index', :project_id => @project
319 redirect_to :action => 'index', :project_id => @project
320 end
320 end
321
322 def destroy_attachment
323 a = @issue.attachments.find(params[:attachment_id])
324 a.destroy
325 journal = @issue.init_journal(User.current)
326 journal.details << JournalDetail.new(:property => 'attachment',
327 :prop_key => a.id,
328 :old_value => a.filename)
329 journal.save
330 redirect_to :action => 'show', :id => @issue
331 end
332
321
333 def gantt
322 def gantt
334 @gantt = Redmine::Helpers::Gantt.new(params)
323 @gantt = Redmine::Helpers::Gantt.new(params)
@@ -188,10 +188,13 class ProjectsController < ApplicationController
188
188
189 def add_file
189 def add_file
190 if request.post?
190 if request.post?
191 @version = @project.versions.find_by_id(params[:version_id])
191 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
192 attachments = attach_files(@version, params[:attachments])
192 attachments = attach_files(container, params[:attachments])
193 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added')
193 if !attachments.empty? && Setting.notified_events.include?('file_added')
194 Mailer.deliver_attachments_added(attachments)
195 end
194 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
196 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
197 return
195 end
198 end
196 @versions = @project.versions.sort
199 @versions = @project.versions.sort
197 end
200 end
@@ -203,7 +206,8 class ProjectsController < ApplicationController
203 'size' => "#{Attachment.table_name}.filesize",
206 'size' => "#{Attachment.table_name}.filesize",
204 'downloads' => "#{Attachment.table_name}.downloads"
207 'downloads' => "#{Attachment.table_name}.downloads"
205
208
206 @versions = @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
209 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
210 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
207 render :layout => !request.xhr?
211 render :layout => !request.xhr?
208 end
212 end
209
213
@@ -37,12 +37,6 class VersionsController < ApplicationController
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' }
@@ -21,7 +21,7 class WikiController < ApplicationController
21 before_filter :find_wiki, :authorize
21 before_filter :find_wiki, :authorize
22 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
22 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
23
23
24 verify :method => :post, :only => [:destroy, :destroy_attachment, :protect], :redirect_to => { :action => :index }
24 verify :method => :post, :only => [:destroy, :protect], :redirect_to => { :action => :index }
25
25
26 helper :attachments
26 helper :attachments
27 include AttachmentsHelper
27 include AttachmentsHelper
@@ -181,13 +181,6 class WikiController < ApplicationController
181 redirect_to :action => 'index', :page => @page.title
181 redirect_to :action => 'index', :page => @page.title
182 end
182 end
183
183
184 def destroy_attachment
185 @page = @wiki.find_page(params[:page])
186 return render_403 unless editable?
187 @page.attachments.find(params[:attachment_id]).destroy
188 redirect_to :action => 'index', :page => @page.title
189 end
190
191 private
184 private
192
185
193 def find_wiki
186 def find_wiki
@@ -16,10 +16,15
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
@@ -98,6 +98,14 class Attachment < ActiveRecord::Base
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
@@ -18,7 +18,7
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}"},
@@ -26,13 +26,13 class Issue < ActiveRecord::Base
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"],
@@ -266,4 +266,15 class Issue < ActiveRecord::Base
266 def to_s
266 def to_s
267 "#{tracker} ##{id}: #{subject}"
267 "#{tracker} ##{id}: #{subject}"
268 end
268 end
269
270 private
271
272 # Callback on attachment deletion
273 def attachment_removed(obj)
274 journal = init_journal(User.current)
275 journal.details << JournalDetail.new(:property => 'attachment',
276 :prop_key => obj.id,
277 :old_value => obj.filename)
278 journal.save
279 end
269 end
280 end
@@ -74,6 +74,9 class Mailer < ActionMailer::Base
74 added_to = ''
74 added_to = ''
75 added_to_url = ''
75 added_to_url = ''
76 case container.class.name
76 case container.class.name
77 when 'Project'
78 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
79 added_to = "#{l(:label_project)}: #{container}"
77 when 'Version'
80 when 'Version'
78 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
81 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
79 added_to = "#{l(:label_version)}: #{container.name}"
82 added_to = "#{l(:label_version)}: #{container.name}"
@@ -19,7 +19,7 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'],
@@ -44,6 +44,8 class Project < ActiveRecord::Base
44 :association_foreign_key => 'custom_field_id'
44 :association_foreign_key => 'custom_field_id'
45
45
46 acts_as_tree :order => "name", :counter_cache => true
46 acts_as_tree :order => "name", :counter_cache => true
47 acts_as_attachable :view_permission => :view_files,
48 :delete_permission => :manage_files
47
49
48 acts_as_customizable
50 acts_as_customizable
49 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
51 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
@@ -19,7 +19,8 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]
@@ -21,7 +21,7 require 'enumerator'
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}"},
@@ -111,6 +111,10 class WikiPage < ActiveRecord::Base
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)
@@ -3,14 +3,14
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>
@@ -12,7 +12,7
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;",
@@ -67,9 +67,7 end %>
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 />
@@ -15,7 +15,7
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
@@ -31,7 +31,7
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 %>
@@ -4,10 +4,13
4 <div class="box">
4 <div class="box">
5 <% form_tag({ :action => 'add_file', :id => @project }, :multipart => true, :class => "tabular") do %>
5 <% form_tag({ :action => 'add_file', :id => @project }, :multipart => true, :class => "tabular") do %>
6
6
7 <p><label for="version_id"><%=l(:field_version)%> <span class="required">*</span></label>
7 <% if @versions.any? %>
8 <%= select_tag "version_id", options_from_collection_for_select(@versions, "id", "name") %></p>
8 <p><label for="version_id"><%=l(:field_version)%></label>
9 <%= select_tag "version_id", content_tag('option', '') +
10 options_from_collection_for_select(@versions, "id", "name") %></p>
11 <% end %>
9
12
10 <p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
13 <p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
11 </div>
14 </div>
12 <%= submit_tag l(:button_add) %>
15 <%= submit_tag l(:button_add) %>
13 <% end %> No newline at end of file
16 <% end %>
@@ -4,39 +4,37
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>
12 <%= sort_header_tag('filename', :caption => l(:field_filename)) %>
11 <%= sort_header_tag('filename', :caption => l(:field_filename)) %>
13 <%= sort_header_tag('created_on', :caption => l(:label_date), :default_order => 'desc') %>
12 <%= sort_header_tag('created_on', :caption => l(:label_date), :default_order => 'desc') %>
14 <%= sort_header_tag('size', :caption => l(:field_filesize), :default_order => 'desc') %>
13 <%= sort_header_tag('size', :caption => l(:field_filesize), :default_order => 'desc') %>
15 <%= sort_header_tag('downloads', :caption => l(:label_downloads_abbr), :default_order => 'desc') %>
14 <%= sort_header_tag('downloads', :caption => l(:label_downloads_abbr), :default_order => 'desc') %>
16 <th>MD5</th>
15 <th>MD5</th>
17 <% if delete_allowed %><th></th><% end %>
16 <th></th>
18 </tr></thead>
17 </tr></thead>
19 <tbody>
18 <tbody>
20 <% for version in @versions %>
19 <% @containers.each do |container| %>
21 <% unless version.attachments.empty? %>
20 <% next if container.attachments.empty? -%>
22 <tr><th colspan="7" align="left"><span class="icon icon-package"><b><%= version.name %></b></span></th></tr>
21 <% if container.is_a?(Version) -%>
23 <% for file in version.attachments %>
22 <tr><th colspan="6" align="left"><span class="icon icon-package"><b><%=h container %></b></span></th></tr>
23 <% end -%>
24 <% container.attachments.each do |file| %>
24 <tr class="<%= cycle("odd", "even") %>">
25 <tr class="<%= cycle("odd", "even") %>">
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 %>
32 <td align="center">
31 <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 %>
32 <%= link_to(image_tag('delete.png'), {:controller => 'attachments', :action => 'destroy', :id => file},
33 :confirm => l(:text_are_you_sure), :method => :post) if delete_allowed %>
34 </td>
34 </td>
35 <% end %>
36 </tr>
35 </tr>
37 <% end
36 <% end
38 reset_cycle %>
37 reset_cycle %>
39 <% end %>
40 <% end %>
38 <% end %>
41 </tbody>
39 </tbody>
42 </table>
40 </table>
@@ -28,7 +28,7
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;",
@@ -8,6 +8,7 http://www.redmine.org/
8 == v0.8.1
8 == v0.8.1
9
9
10 * Select watchers on new issue form
10 * Select watchers on new issue form
11 * Files module: ability to add files without version
11 * Show view/annotate/download links on entry and annotate views
12 * Show view/annotate/download links on entry and annotate views
12 * Fixed: Deleted files are shown when using Darcs
13 * Fixed: Deleted files are shown when using Darcs
13
14
@@ -35,7 +35,7 Redmine::AccessControl.map do |map|
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
@@ -67,12 +67,12 Redmine::AccessControl.map do |map|
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
@@ -83,7 +83,7 Redmine::AccessControl.map do |map|
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
@@ -85,4 +85,28 attachments_007:
85 filename: archive.zip
85 filename: archive.zip
86 author_id: 1
86 author_id: 1
87 content_type: application/octet-stream
87 content_type: application/octet-stream
88 attachments_008:
89 created_on: 2006-07-19 21:07:27 +02:00
90 container_type: Project
91 container_id: 1
92 downloads: 0
93 disk_filename: 060719210727_project_file.zip
94 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
95 id: 8
96 filesize: 320
97 filename: project_file.zip
98 author_id: 2
99 content_type: application/octet-stream
100 attachments_009:
101 created_on: 2006-07-19 21:07:27 +02:00
102 container_type: Version
103 container_id: 1
104 downloads: 0
105 disk_filename: 060719210727_version_file.zip
106 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
107 id: 9
108 filesize: 452
109 filename: version_file.zip
110 author_id: 2
111 content_type: application/octet-stream
88 No newline at end of file
112
@@ -76,4 +76,50 class AttachmentsControllerTest < Test::Unit::TestCase
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 assert_response 302
101 end
102 end
103
104 def test_destroy_project_attachment
105 @request.session[:user_id] = 2
106 assert_difference 'Attachment.count', -1 do
107 post :destroy, :id => 8
108 assert_response 302
109 end
110 end
111
112 def test_destroy_version_attachment
113 @request.session[:user_id] = 2
114 assert_difference 'Attachment.count', -1 do
115 post :destroy, :id => 9
116 assert_response 302
117 end
118 end
119
120 def test_destroy_without_permission
121 post :destroy, :id => 3
122 assert_redirected_to '/login'
123 assert Attachment.find_by_id(3)
124 end
79 end
125 end
@@ -747,17 +747,4 class IssuesControllerTest < Test::Unit::TestCase
747 assert_equal 2, TimeEntry.find(1).issue_id
747 assert_equal 2, TimeEntry.find(1).issue_id
748 assert_equal 2, TimeEntry.find(2).issue_id
748 assert_equal 2, TimeEntry.find(2).issue_id
749 end
749 end
750
751 def test_destroy_attachment
752 issue = Issue.find(3)
753 a = issue.attachments.size
754 @request.session[:user_id] = 2
755 post :destroy_attachment, :id => 3, :attachment_id => 1
756 assert_redirected_to 'issues/show/3'
757 assert_nil Attachment.find_by_id(1)
758 issue.reload
759 assert_equal((a-1), issue.attachments.size)
760 j = issue.journals.find(:first, :order => 'created_on DESC')
761 assert_equal 'attachment', j.details.first.property
762 end
763 end
750 end
@@ -64,7 +64,7 class MessagesControllerTest < Test::Unit::TestCase
64 def test_post_new
64 def test_post_new
65 @request.session[:user_id] = 2
65 @request.session[:user_id] = 2
66 ActionMailer::Base.deliveries.clear
66 ActionMailer::Base.deliveries.clear
67 Setting.notified_events << 'message_posted'
67 Setting.notified_events = ['message_posted']
68
68
69 post :new, :board_id => 1,
69 post :new, :board_id => 1,
70 :message => { :subject => 'Test created message',
70 :message => { :subject => 'Test created message',
@@ -23,7 +23,8 class ProjectsController; def rescue_action(e) raise e end; end
23
23
24 class ProjectsControllerTest < Test::Unit::TestCase
24 class ProjectsControllerTest < Test::Unit::TestCase
25 fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details,
25 fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details,
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
27 :attachments
27
28
28 def setup
29 def setup
29 @controller = ProjectsController.new
30 @controller = ProjectsController.new
@@ -112,12 +113,56 class ProjectsControllerTest < Test::Unit::TestCase
112 assert_redirected_to 'admin/projects'
113 assert_redirected_to 'admin/projects'
113 assert_nil Project.find_by_id(1)
114 assert_nil Project.find_by_id(1)
114 end
115 end
116
117 def test_add_file
118 set_tmp_attachments_directory
119 @request.session[:user_id] = 2
120 Setting.notified_events = ['file_added']
121 ActionMailer::Base.deliveries.clear
122
123 assert_difference 'Attachment.count' do
124 post :add_file, :id => 1, :version_id => '',
125 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
126 end
127 assert_redirected_to 'projects/list_files/ecookbook'
128 a = Attachment.find(:first, :order => 'created_on DESC')
129 assert_equal 'testfile.txt', a.filename
130 assert_equal Project.find(1), a.container
131
132 mail = ActionMailer::Base.deliveries.last
133 assert_kind_of TMail::Mail, mail
134 assert_equal "[eCookbook] New file", mail.subject
135 assert mail.body.include?('testfile.txt')
136 end
137
138 def test_add_version_file
139 set_tmp_attachments_directory
140 @request.session[:user_id] = 2
141 Setting.notified_events = ['file_added']
142
143 assert_difference 'Attachment.count' do
144 post :add_file, :id => 1, :version_id => '2',
145 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
146 end
147 assert_redirected_to 'projects/list_files/ecookbook'
148 a = Attachment.find(:first, :order => 'created_on DESC')
149 assert_equal 'testfile.txt', a.filename
150 assert_equal Version.find(2), a.container
151 end
115
152
116 def test_list_files
153 def test_list_files
117 get :list_files, :id => 1
154 get :list_files, :id => 1
118 assert_response :success
155 assert_response :success
119 assert_template 'list_files'
156 assert_template 'list_files'
120 assert_not_nil assigns(:versions)
157 assert_not_nil assigns(:containers)
158
159 # file attached to the project
160 assert_tag :a, :content => 'project_file.zip',
161 :attributes => { :href => '/attachments/download/8/project_file.zip' }
162
163 # file attached to a project's version
164 assert_tag :a, :content => 'version_file.zip',
165 :attributes => { :href => '/attachments/download/9/version_file.zip' }
121 end
166 end
122
167
123 def test_changelog
168 def test_changelog
@@ -252,13 +252,6 class WikiControllerTest < Test::Unit::TestCase
252 assert_template 'edit'
252 assert_template 'edit'
253 end
253 end
254
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
262 def test_history_of_non_existing_page_should_return_404
255 def test_history_of_non_existing_page_should_return_404
263 get :history, :id => 1, :page => 'Unknown_page'
256 get :history, :id => 1, :page => 'Unknown_page'
264 assert_response 404
257 assert_response 404
General Comments 0
You need to be logged in to leave comments. Login now