##// END OF EJS Templates
Fixed: Files without Version aren't visible in the Activity page (#2930)....
Jean-Philippe Lang -
r2501:4f4d447224a4
parent child
Show More
@@ -1,151 +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 OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_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)
101 def visible?(user=User.current)
102 container.attachments_visible?(user)
102 container.attachments_visible?(user)
103 end
103 end
104
104
105 def deletable?(user=User.current)
105 def deletable?(user=User.current)
106 container.attachments_deletable?(user)
106 container.attachments_deletable?(user)
107 end
107 end
108
108
109 def image?
109 def image?
110 self.filename =~ /\.(jpe?g|gif|png)$/i
110 self.filename =~ /\.(jpe?g|gif|png)$/i
111 end
111 end
112
112
113 def is_text?
113 def is_text?
114 Redmine::MimeType.is_type?('text', filename)
114 Redmine::MimeType.is_type?('text', filename)
115 end
115 end
116
116
117 def is_diff?
117 def is_diff?
118 self.filename =~ /\.(patch|diff)$/i
118 self.filename =~ /\.(patch|diff)$/i
119 end
119 end
120
120
121 private
121 private
122 def sanitize_filename(value)
122 def sanitize_filename(value)
123 # get only the filename, not the whole path
123 # get only the filename, not the whole path
124 just_filename = value.gsub(/^.*(\\|\/)/, '')
124 just_filename = value.gsub(/^.*(\\|\/)/, '')
125 # 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
126 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
126 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
127
127
128 # Finally, replace all non alphanumeric, hyphens or periods with underscore
128 # Finally, replace all non alphanumeric, hyphens or periods with underscore
129 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
129 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
130 end
130 end
131
131
132 # Returns an ASCII or hashed filename
132 # Returns an ASCII or hashed filename
133 def self.disk_filename(filename)
133 def self.disk_filename(filename)
134 df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
134 df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
135 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
135 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
136 df << filename
136 df << filename
137 else
137 else
138 df << Digest::MD5.hexdigest(filename)
138 df << Digest::MD5.hexdigest(filename)
139 # keep the extension if any
139 # keep the extension if any
140 df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
140 df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
141 end
141 end
142 df
142 df
143 end
143 end
144
144
145 # Returns the MD5 digest of the file at given path
145 # Returns the MD5 digest of the file at given path
146 def self.digest(filename)
146 def self.digest(filename)
147 File.open(filename, 'rb') do |f|
147 File.open(filename, 'rb') do |f|
148 Digest::MD5.hexdigest(f.read)
148 Digest::MD5.hexdigest(f.read)
149 end
149 end
150 end
150 end
151 end
151 end
@@ -1,80 +1,92
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
19
20 class ActivityTest < Test::Unit::TestCase
20 class ActivityTest < Test::Unit::TestCase
21 fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details,
21 fixtures :projects, :versions, :attachments, :users, :roles, :members, :issues, :journals, :journal_details,
22 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages
22 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages
23
23
24 def setup
24 def setup
25 @project = Project.find(1)
25 @project = Project.find(1)
26 end
26 end
27
27
28 def test_activity_without_subprojects
28 def test_activity_without_subprojects
29 events = find_events(User.anonymous, :project => @project)
29 events = find_events(User.anonymous, :project => @project)
30 assert_not_nil events
30 assert_not_nil events
31
31
32 assert events.include?(Issue.find(1))
32 assert events.include?(Issue.find(1))
33 assert !events.include?(Issue.find(4))
33 assert !events.include?(Issue.find(4))
34 # subproject issue
34 # subproject issue
35 assert !events.include?(Issue.find(5))
35 assert !events.include?(Issue.find(5))
36 end
36 end
37
37
38 def test_activity_with_subprojects
38 def test_activity_with_subprojects
39 events = find_events(User.anonymous, :project => @project, :with_subprojects => 1)
39 events = find_events(User.anonymous, :project => @project, :with_subprojects => 1)
40 assert_not_nil events
40 assert_not_nil events
41
41
42 assert events.include?(Issue.find(1))
42 assert events.include?(Issue.find(1))
43 # subproject issue
43 # subproject issue
44 assert events.include?(Issue.find(5))
44 assert events.include?(Issue.find(5))
45 end
45 end
46
46
47 def test_global_activity_anonymous
47 def test_global_activity_anonymous
48 events = find_events(User.anonymous)
48 events = find_events(User.anonymous)
49 assert_not_nil events
49 assert_not_nil events
50
50
51 assert events.include?(Issue.find(1))
51 assert events.include?(Issue.find(1))
52 assert events.include?(Message.find(5))
52 assert events.include?(Message.find(5))
53 # Issue of a private project
53 # Issue of a private project
54 assert !events.include?(Issue.find(4))
54 assert !events.include?(Issue.find(4))
55 end
55 end
56
56
57 def test_global_activity_logged_user
57 def test_global_activity_logged_user
58 events = find_events(User.find(2)) # manager
58 events = find_events(User.find(2)) # manager
59 assert_not_nil events
59 assert_not_nil events
60
60
61 assert events.include?(Issue.find(1))
61 assert events.include?(Issue.find(1))
62 # Issue of a private project the user belongs to
62 # Issue of a private project the user belongs to
63 assert events.include?(Issue.find(4))
63 assert events.include?(Issue.find(4))
64 end
64 end
65
65
66 def test_user_activity
66 def test_user_activity
67 user = User.find(2)
67 user = User.find(2)
68 events = Redmine::Activity::Fetcher.new(User.anonymous, :author => user).events(nil, nil, :limit => 10)
68 events = Redmine::Activity::Fetcher.new(User.anonymous, :author => user).events(nil, nil, :limit => 10)
69
69
70 assert(events.size > 0)
70 assert(events.size > 0)
71 assert(events.size <= 10)
71 assert(events.size <= 10)
72 assert_nil(events.detect {|e| e.event_author != user})
72 assert_nil(events.detect {|e| e.event_author != user})
73 end
73 end
74
74
75 def test_files_activity
76 f = Redmine::Activity::Fetcher.new(User.anonymous, :project => Project.find(1))
77 f.scope = ['files']
78 events = f.events
79
80 assert_kind_of Array, events
81 assert events.include?(Attachment.find_by_container_type_and_container_id('Project', 1))
82 assert events.include?(Attachment.find_by_container_type_and_container_id('Version', 1))
83 assert_equal [Attachment], events.collect(&:class).uniq
84 assert_equal %w(Project Version), events.collect(&:container_type).uniq.sort
85 end
86
75 private
87 private
76
88
77 def find_events(user, options={})
89 def find_events(user, options={})
78 Redmine::Activity::Fetcher.new(user, options).events(Date.today - 30, Date.today + 1)
90 Redmine::Activity::Fetcher.new(user, options).events(Date.today - 30, Date.today + 1)
79 end
91 end
80 end
92 end
General Comments 0
You need to be logged in to leave comments. Login now